diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt b/core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt
index d2df0a12..b2622c9a 100644
--- a/core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/button/AppRoundButton.kt
@@ -26,9 +26,9 @@ import com.twix.domain.model.enums.AppTextStyle
fun AppRoundButton(
text: String,
textColor: Color,
+ backgroundColor: Color,
modifier: Modifier = Modifier,
textStyle: AppTextStyle = AppTextStyle.T2,
- backgroundColor: Color,
borderColor: Color = GrayColor.C500,
hasBorder: Boolean = true,
) {
diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt
new file mode 100644
index 00000000..cce5a1ea
--- /dev/null
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentAnchorFrame.kt
@@ -0,0 +1,105 @@
+package com.twix.designsystem.components.comment
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.twix.designsystem.components.comment.model.CommentUiModel
+import com.twix.designsystem.theme.DimmedColor
+import com.twix.ui.extension.noRippleClickable
+
+/**
+ * 특정 UI 요소(Anchor) 하단에 [CommentBox]를 배치하고, 키보드 활성화 상태에 따라 위치를 동적으로 조정하는 프레임 컴포저블
+ *
+ * 이 컴포저블은 평상시에는 [anchorBottom] 좌표를 기준으로 배치
+ * 키보드가 올라와 코맨트창이 가려질 경우 키보드 바로 위로 위치를 자동으로 이동
+ *
+ * @param uiModel 댓글창의 상태(텍스트, 포커스 상태)를 담고 있는 데이터 모델
+ * @param anchorBottom 댓글창 배치의 기준이 되는 상위 요소의 바닥(Bottom) Y 좌표 (px 단위)
+ * @param onCommentChanged 댓글 내용이 변경될 때 호출되는 콜백
+ * @param onFocusChanged 댓글창의 포커스 상태가 변경될 때 호출되는 콜백 (포커스 시 배경 딤 처리 등에 사용)
+ * @param modifier 레이아웃 수정을 위한 [Modifier]
+ */
+@Composable
+fun CommentAnchorFrame(
+ uiModel: CommentUiModel,
+ anchorBottom: Float,
+ onCommentChanged: (String) -> Unit,
+ onFocusChanged: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ if (anchorBottom == 0f) return
+
+ val density = LocalDensity.current
+ val focusManager = LocalFocusManager.current
+ val imeBottom = WindowInsets.ime.getBottom(density)
+ val paddingBottom = with(density) { 24.dp.toPx() }
+
+ var commentBoxHeight by remember { mutableFloatStateOf(0f) }
+ val defaultY = anchorBottom - commentBoxHeight - paddingBottom
+
+ BoxWithConstraints(modifier = modifier.fillMaxSize()) {
+ val screenHeight = constraints.maxHeight.toFloat()
+ val keyboardTop = screenHeight - imeBottom
+
+ AnimatedVisibility(
+ visible = uiModel.isFocused,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .background(DimmedColor.D070)
+ .noRippleClickable { focusManager.clearFocus() },
+ )
+ }
+ CommentBox(
+ uiModel = uiModel,
+ onCommentChanged = onCommentChanged,
+ onFocusChanged = onFocusChanged,
+ onHeightMeasured = { height ->
+ if (commentBoxHeight != height) commentBoxHeight = height
+ },
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .offset {
+ /**
+ * 키보드가 활성화되었고, 댓글창이 키보드 위치보다 아래에 있어 가려지는 경우
+ * */
+ val targetY =
+ if (imeBottom > 0 && (defaultY + commentBoxHeight) > keyboardTop) {
+ /**
+ * 화면 전체 높이 - 키보드 높이 - 코멘트 높이
+ * */
+ (screenHeight - imeBottom - commentBoxHeight).toInt()
+ } else {
+ /**
+ * 키보드가 없거나, 댓글창이 키보드에 가려지지 않는 경우
+ * */
+ defaultY.toInt()
+ }
+ IntOffset(x = 0, y = targetY)
+ },
+ )
+ }
+}
diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt
index 595136bd..1eaa61a6 100644
--- a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt
@@ -3,10 +3,10 @@ package com.twix.designsystem.components.comment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-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.layout.onSizeChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.twix.designsystem.R
@@ -20,11 +20,15 @@ fun CommentBox(
uiModel: CommentUiModel,
onCommentChanged: (String) -> Unit,
onFocusChanged: (Boolean) -> Unit,
+ onHeightMeasured: (Float) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
- modifier = modifier,
+ modifier =
+ modifier.onSizeChanged { size ->
+ onHeightMeasured(size.height.toFloat())
+ },
) {
AppText(
text = if (uiModel.isFocused) stringResource(R.string.comment_condition_guide) else "",
@@ -33,13 +37,11 @@ fun CommentBox(
)
Spacer(modifier = Modifier.height(8.dp))
+
CommentTextField(
uiModel = uiModel,
onCommitComment = onCommentChanged,
onFocusChanged = onFocusChanged,
- modifier =
- Modifier
- .padding(bottom = 20.dp),
)
}
}
diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt
index d76fdd50..8f3ca856 100644
--- a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt
@@ -21,13 +21,12 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -50,13 +49,30 @@ fun CommentTextField(
enabled: Boolean = true,
onCommitComment: (String) -> Unit = {},
onFocusChanged: (Boolean) -> Unit = {},
- onPositioned: (Rect) -> Unit = {},
) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val keyboardState by keyboardAsState()
- var internalValue by rememberSaveable(uiModel.comment) { mutableStateOf(uiModel.comment) }
+ var internalValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(
+ TextFieldValue(
+ text = uiModel.value,
+ selection = TextRange(uiModel.value.length),
+ ),
+ )
+ }
+
+ LaunchedEffect(uiModel.value) {
+ if (uiModel.value != internalValue.text) {
+ internalValue =
+ TextFieldValue(
+ text = uiModel.value,
+ selection = TextRange(uiModel.value.length),
+ )
+ }
+ }
+
var isInitialized by remember { mutableStateOf(false) }
LaunchedEffect(keyboardState) {
@@ -66,7 +82,7 @@ fun CommentTextField(
when (keyboardState) {
Keyboard.Opened -> Unit
Keyboard.Closed -> {
- onCommitComment(internalValue.trim())
+ onCommitComment(internalValue.text.trim())
focusManager.clearFocus()
}
}
@@ -74,23 +90,28 @@ fun CommentTextField(
}
LaunchedEffect(uiModel.isFocused) {
+ /**
+ * 외부에서 포커스 상태를 제어하는 경우
+ * e.g 사진 촬영 & 사진 선택
+ * */
if (uiModel.isFocused) focusRequester.requestFocus()
}
Box(
modifier =
modifier
- .onGloballyPositioned { coordinates ->
- onPositioned(coordinates.boundsInRoot())
- }.noRippleClickable {
+ .noRippleClickable {
focusRequester.requestFocus()
},
) {
TextField(
value = internalValue,
onValueChange = { newValue ->
- if (newValue.length <= CommentUiModel.COMMENT_COUNT) {
- internalValue = newValue
+ if (newValue.text.length <= CommentUiModel.COMMENT_COUNT) {
+ internalValue =
+ newValue.copy(
+ selection = TextRange(newValue.text.length),
+ )
}
},
enabled = enabled,
@@ -130,16 +151,16 @@ fun CommentTextField(
) {
repeat(CommentUiModel.COMMENT_COUNT) { index ->
val char =
- if (uiModel.isFocused || internalValue.isNotEmpty()) {
- internalValue.getOrNull(index)?.toString()
+ if (uiModel.isFocused || internalValue.text.isNotEmpty()) {
+ internalValue.text.getOrNull(index)?.toString()
} else {
stringResource(R.string.comment_text_field_placeholder)[index].toString()
}.orEmpty()
CommentCircle(
text = char,
- showPlaceholder = !uiModel.isFocused && internalValue.isEmpty(),
- showCursor = uiModel.isFocused && index == internalValue.length,
+ showPlaceholder = !uiModel.isFocused && internalValue.text.isEmpty(),
+ showCursor = uiModel.isFocused && index == internalValue.text.length,
modifier =
Modifier.noRippleClickable {
focusRequester.requestFocus()
@@ -159,7 +180,6 @@ private fun CommentTextFieldPreview() {
CommentTextField(
uiModel = CommentUiModel(text, isFocused),
onFocusChanged = { isFocused = it },
- onPositioned = {},
)
}
}
diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt b/core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt
index e44e5675..284c0bad 100644
--- a/core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt
@@ -4,19 +4,19 @@ import androidx.compose.runtime.Immutable
@Immutable
data class CommentUiModel(
- val comment: String = "",
+ val value: String = "",
val isFocused: Boolean = false,
) {
val hasMaxCommentLength: Boolean
- get() = comment.length == COMMENT_COUNT
+ get() = value.length == COMMENT_COUNT
val canUpload: Boolean
get() =
- comment.isEmpty() ||
- comment.isNotEmpty() &&
+ value.isEmpty() ||
+ value.isNotEmpty() &&
hasMaxCommentLength
- fun updateComment(newComment: String): CommentUiModel = copy(comment = newComment)
+ fun updateComment(newComment: String): CommentUiModel = copy(value = newComment)
fun updateFocus(isFocused: Boolean) = copy(isFocused = isFocused)
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/BackgroundCard.kt b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt
similarity index 90%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/component/BackgroundCard.kt
rename to core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt
index 284dcfb4..ab898f48 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/BackgroundCard.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.component
+package com.twix.designsystem.components.photolog
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
@@ -16,15 +16,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.twix.designsystem.R
import com.twix.designsystem.components.button.AppRoundButton
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
-import com.twix.task_certification.R
import com.twix.ui.extension.noRippleClickable
-import com.twix.designsystem.R as DesR
@Composable
fun BackgroundCard(
@@ -69,7 +68,7 @@ fun BackgroundCard(
}
Image(
- imageVector = ImageVector.vectorResource(DesR.drawable.ic_keepi_sting),
+ imageVector = ImageVector.vectorResource(R.drawable.ic_keepi_sting),
contentDescription = null,
modifier =
Modifier
@@ -86,7 +85,7 @@ fun BackgroundCard(
fun PreviewBackgroundCard() {
TwixTheme {
BackgroundCard(
- buttonTitle = stringResource(R.string.task_certification_detail_partner_sting),
+ buttonTitle = stringResource(R.string.word_sting),
uploadedAt = "2023.10.31 23:59",
onClick = {},
isCertificated = true,
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/CertificatedCard.kt b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt
similarity index 95%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/component/CertificatedCard.kt
rename to core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt
index 51967e1e..9d2ee54a 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/CertificatedCard.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/CertificatedCard.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.component
+package com.twix.designsystem.components.photolog
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -18,7 +18,7 @@ import com.twix.designsystem.components.comment.model.CommentUiModel
import com.twix.designsystem.theme.TwixTheme
@Composable
-internal fun CertificatedCard(
+fun CertificatedCard(
imageUrl: String?,
comment: String?,
) {
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ForegroundCard.kt b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/ForegroundCard.kt
similarity index 79%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ForegroundCard.kt
rename to core/design-system/src/main/java/com/twix/designsystem/components/photolog/ForegroundCard.kt
index 4d4851a9..7bc42d08 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ForegroundCard.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/ForegroundCard.kt
@@ -1,19 +1,18 @@
-package com.twix.task_certification.detail.component
+package com.twix.designsystem.components.photolog
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import com.twix.designsystem.R
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
import com.twix.domain.model.enums.BetweenUs
-import com.twix.task_certification.R
-import com.twix.designsystem.R as DesR
@Composable
-internal fun ForegroundCard(
+fun ForegroundCard(
isCertificated: Boolean,
nickName: String,
imageUrl: String?,
@@ -32,9 +31,11 @@ internal fun ForegroundCard(
AppText(
text =
when (currentShow) {
- BetweenUs.ME -> stringResource(DesR.string.keep_it_up)
+ BetweenUs.ME -> stringResource(R.string.keep_it_up)
BetweenUs.PARTNER ->
- stringResource(R.string.task_certification_detail_partner_not_task_certification).format(nickName)
+ stringResource(R.string.partner_not_task_certification).format(
+ nickName,
+ )
},
style = AppTextStyle.H2,
color = GrayColor.C500,
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/PhotologCard.kt b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/PhotologCard.kt
similarity index 87%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/component/PhotologCard.kt
rename to core/design-system/src/main/java/com/twix/designsystem/components/photolog/PhotologCard.kt
index fd1348fc..c89f068b 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/PhotologCard.kt
+++ b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/PhotologCard.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.component
+package com.twix.designsystem.components.photolog
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -16,13 +16,15 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.twix.designsystem.theme.CommonColor
+import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
@Composable
-internal fun PhotologCard(
- background: Color,
- borderColor: Color,
+fun PhotologCard(
modifier: Modifier = Modifier,
+ borderColor: Color = GrayColor.C500,
+ background: Color = CommonColor.White,
rotation: Float = 0f,
content: @Composable BoxScope.() -> Unit = {},
) {
diff --git a/feature/task-certification/src/main/res/drawable/ic_camara_rotate.xml b/core/design-system/src/main/res/drawable/ic_camara_rotate.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camara_rotate.xml
rename to core/design-system/src/main/res/drawable/ic_camara_rotate.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_camera_retake.xml b/core/design-system/src/main/res/drawable/ic_camera_retake.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camera_retake.xml
rename to core/design-system/src/main/res/drawable/ic_camera_retake.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_camera_shutter.xml b/core/design-system/src/main/res/drawable/ic_camera_shutter.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camera_shutter.xml
rename to core/design-system/src/main/res/drawable/ic_camera_shutter.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_camera_toggle.xml b/core/design-system/src/main/res/drawable/ic_camera_toggle.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camera_toggle.xml
rename to core/design-system/src/main/res/drawable/ic_camera_toggle.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_camera_torch_off.xml b/core/design-system/src/main/res/drawable/ic_camera_torch_off.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camera_torch_off.xml
rename to core/design-system/src/main/res/drawable/ic_camera_torch_off.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_camera_torch_on.xml b/core/design-system/src/main/res/drawable/ic_camera_torch_on.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_camera_torch_on.xml
rename to core/design-system/src/main/res/drawable/ic_camera_torch_on.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_close_c100.xml b/core/design-system/src/main/res/drawable/ic_close_c100.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_close_c100.xml
rename to core/design-system/src/main/res/drawable/ic_close_c100.xml
diff --git a/feature/task-certification/src/main/res/drawable/ic_gallery.xml b/core/design-system/src/main/res/drawable/ic_gallery.xml
similarity index 100%
rename from feature/task-certification/src/main/res/drawable/ic_gallery.xml
rename to core/design-system/src/main/res/drawable/ic_gallery.xml
diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml
index 6d338ec8..953d5f4f 100644
--- a/core/design-system/src/main/res/values/strings.xml
+++ b/core/design-system/src/main/res/values/strings.xml
@@ -15,10 +15,12 @@
취소
삭제
수정
+ 저장
설정
계정
정보
로그아웃
+ 찌르기
매일
매주
@@ -48,6 +50,9 @@
코멘트추가
5글자로 코멘트를 남길 수 있어요
+
+ %s님은\n아직…
+
목표 직접 만들기
목표 수정하기
@@ -93,4 +98,26 @@
정말 탈퇴하시겠어요?
커플 연결이 끊어집니다.\n데이터는 전부 삭제되며 복구가 불가능합니다.
+
+ 업로드
+ 이미지 캡처에 실패했습니다. 다시 시도해 주세요.
+ 이미지를 불러오는 데 실패했습니다. 다시 시도해 주세요.
+ 인증샷 찍기
+ 인증샷 등록에 실패했습니다.
+ 이미지 변환에 실패했습니다.
+ 코멘트는 5글자로 입력해주세요!
+
+
+ 인증샷 조회에 실패했습니다.
+ 인증샷 수정에 실패했어요.
+
+
+ 다시 찍기
+ 코멘트가 수정 되었어요.
+ 코멘트가 수정되지 않았어요.
+ 코멘트 수정에 실패했어요.
+ 찌르기
+ 리액션 요청에 실패했어요.
+
+ 인증샷 촬영을 위해서 카메라 권한이 필요해요.
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index 9f20a596..a9a48887 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -2,8 +2,14 @@ plugins {
alias(libs.plugins.twix.android.library)
alias(libs.plugins.twix.android.compose)
alias(libs.plugins.twix.koin)
+ alias(libs.plugins.serialization)
+ alias(libs.plugins.twix.kermit)
}
android {
namespace = "com.twix.navigation"
}
+
+dependencies {
+ implementation(libs.kotlinx.serialization.json)
+}
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 4432d461..21b5bd01 100644
--- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt
+++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt
@@ -1,5 +1,9 @@
package com.twix.navigation
+import android.net.Uri
+import com.twix.navigation.args.DetailNavArgs
+import com.twix.navigation.args.EditorNavArgs
+import kotlinx.serialization.json.Json
import java.time.LocalDate
/**
@@ -44,19 +48,31 @@ sealed class NavRoutes(
) = "task_certification_detail/$goalId/$date/$betweenUs"
}
- object TaskCertificationRoute : NavRoutes("task_certification/{goalId}/{from}") {
- const val ARG_GOAL_ID = "goalId"
- const val ARG_FROM = "from"
+ object TaskCertificationRoute : NavRoutes("task_certification/{data}") {
+ const val ARG_DATA = "data"
enum class From {
HOME,
DETAIL,
+ EDITOR,
}
- fun createRoute(
- goalId: Long,
- from: From,
- ) = "task_certification/$goalId/${from.name}"
+ fun createRoute(data: DetailNavArgs): String {
+ val json = Json.encodeToString(data)
+ val encoded = Uri.encode(json)
+ return "task_certification/$encoded"
+ }
+ }
+
+ object TaskCertificationEditorRoute :
+ NavRoutes("task_certification_editor/{data}") {
+ const val ARG_DATA = "data"
+
+ fun createRoute(data: EditorNavArgs): String {
+ val json = Json.encodeToString(data)
+ val encoded = Uri.encode(json)
+ return "task_certification_editor/$encoded"
+ }
}
/**
diff --git a/core/navigation/src/main/java/com/twix/navigation/args/TaskCertificationNavArgs.kt b/core/navigation/src/main/java/com/twix/navigation/args/TaskCertificationNavArgs.kt
new file mode 100644
index 00000000..ec21a755
--- /dev/null
+++ b/core/navigation/src/main/java/com/twix/navigation/args/TaskCertificationNavArgs.kt
@@ -0,0 +1,24 @@
+package com.twix.navigation.args
+
+import com.twix.navigation.NavRoutes
+import kotlinx.serialization.Serializable
+
+// TODO("인증샷 단일 조회 API 연동시 제거")
+@Serializable
+data class EditorNavArgs(
+ val goalId: Long,
+ val goalName: String,
+ val nickname: String,
+ val photologId: Long,
+ val imageUrl: String,
+ val comment: String?,
+)
+
+@Serializable
+data class DetailNavArgs(
+ val goalId: Long,
+ val from: NavRoutes.TaskCertificationRoute.From,
+ val photologId: Long = -1,
+ val selectedDate: String = "",
+ val comment: String = "",
+)
diff --git a/core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt b/core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt
new file mode 100644
index 00000000..68c3b785
--- /dev/null
+++ b/core/navigation/src/main/java/com/twix/navigation/savedstate/SafeDecode.kt
@@ -0,0 +1,23 @@
+package com.twix.navigation.savedstate
+
+import android.net.Uri
+import androidx.lifecycle.SavedStateHandle
+import co.touchlab.kermit.Logger
+import kotlinx.serialization.json.Json
+
+inline fun SavedStateHandle.decodeNavArgs(key: String): T {
+ val raw = get(key)
+
+ try {
+ val decoded =
+ raw?.let(Uri::decode)
+ ?: error("Missing nav arg: $key")
+
+ Logger.d { "NavArgs[$key] decoded=$decoded" }
+
+ return Json.decodeFromString(decoded)
+ } catch (e: Exception) {
+ Logger.e(e) { "NavArgs[$key] decode failed raw=$raw" }
+ throw e
+ }
+}
diff --git a/core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt b/core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt
new file mode 100644
index 00000000..b839bed8
--- /dev/null
+++ b/core/network/src/main/java/com/twix/network/model/request/photolog/model/PhotologModifyRequest.kt
@@ -0,0 +1,10 @@
+package com.twix.network.model.request.photolog.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PhotologModifyRequest(
+ @SerialName("fileName") val fileName: String,
+ @SerialName("comment") val comment: String,
+)
diff --git a/core/network/src/main/java/com/twix/network/service/PhotoLogService.kt b/core/network/src/main/java/com/twix/network/service/PhotoLogService.kt
index 010284b5..65bfc598 100644
--- a/core/network/src/main/java/com/twix/network/service/PhotoLogService.kt
+++ b/core/network/src/main/java/com/twix/network/service/PhotoLogService.kt
@@ -1,6 +1,7 @@
package com.twix.network.service
import com.twix.network.model.request.ReactionRequest
+import com.twix.network.model.request.photolog.model.PhotologModifyRequest
import com.twix.network.model.request.photolog.model.PhotologRequest
import com.twix.network.model.response.photo.model.PhotoLogUploadUrlResponse
import com.twix.network.model.response.photolog.PhotoLogsResponse
@@ -10,6 +11,7 @@ import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
+import java.time.LocalDate
interface PhotoLogService {
@GET("api/v1/photologs/upload-url")
@@ -24,7 +26,7 @@ interface PhotoLogService {
@GET("api/v1/photologs")
suspend fun fetchPhotoLogs(
- @Query("targetDate") request: String,
+ @Query("targetDate") request: LocalDate,
): PhotoLogsResponse
@PUT("api/v1/photologs/{photologId}/reaction")
@@ -32,4 +34,10 @@ interface PhotoLogService {
@Path("photologId") photologId: Long,
@Body request: ReactionRequest,
)
+
+ @PUT("api/v1/photologs/{photologId}")
+ suspend fun modifyPhotolog(
+ @Path("photologId") photologId: Long,
+ @Body request: PhotologModifyRequest,
+ )
}
diff --git a/core/util/src/main/java/com/twix/util/bus/TaskCertificationRefreshBus.kt b/core/util/src/main/java/com/twix/util/bus/TaskCertificationRefreshBus.kt
index f7a55423..578d3a57 100644
--- a/core/util/src/main/java/com/twix/util/bus/TaskCertificationRefreshBus.kt
+++ b/core/util/src/main/java/com/twix/util/bus/TaskCertificationRefreshBus.kt
@@ -5,13 +5,18 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class TaskCertificationRefreshBus {
+ enum class Publisher {
+ PHOTOLOG,
+ EDITOR,
+ }
+
private val _events =
- MutableSharedFlow(
+ MutableSharedFlow(
replay = 0,
extraBufferCapacity = 1,
)
- val events: SharedFlow = _events.asSharedFlow()
+ val events: SharedFlow = _events.asSharedFlow()
- fun notifyChanged() = _events.tryEmit(Unit)
+ fun notifyChanged(value: Publisher) = _events.tryEmit(value)
}
diff --git a/data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt b/data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt
index e6173831..7e94b786 100644
--- a/data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt
+++ b/data/src/main/java/com/twix/data/repository/DefaultPhotoLogRepository.kt
@@ -8,11 +8,13 @@ import com.twix.domain.repository.PhotoLogRepository
import com.twix.network.execute.safeApiCall
import com.twix.network.model.request.ReactionRequest
import com.twix.network.model.request.photolog.mapper.toRequest
+import com.twix.network.model.request.photolog.model.PhotologModifyRequest
import com.twix.network.model.response.photo.mapper.toDomain
import com.twix.network.model.response.photolog.mapper.toDomain
import com.twix.network.service.PhotoLogService
import com.twix.network.upload.PresignedUploader
import com.twix.result.AppResult
+import java.time.LocalDate
class DefaultPhotoLogRepository(
private val service: PhotoLogService,
@@ -20,10 +22,10 @@ class DefaultPhotoLogRepository(
) : PhotoLogRepository {
override suspend fun getUploadUrl(goalId: Long): AppResult = safeApiCall { service.getUploadUrl(goalId).toDomain() }
- override suspend fun uploadPhotoLog(photologParam: PhotologParam): AppResult =
+ override suspend fun uploadPhotolog(photologParam: PhotologParam): AppResult =
safeApiCall { service.uploadPhotoLog(photologParam.toRequest()) }
- override suspend fun uploadPhotoLogImage(
+ override suspend fun uploadPhotologImage(
goalId: Long,
bytes: ByteArray,
contentType: String,
@@ -49,7 +51,7 @@ class DefaultPhotoLogRepository(
return AppResult.Success(info.fileName)
}
- override suspend fun fetchPhotoLogs(targetDate: String): AppResult =
+ override suspend fun fetchPhotologs(targetDate: LocalDate): AppResult =
safeApiCall {
service.fetchPhotoLogs(targetDate).toDomain()
}
@@ -58,4 +60,10 @@ class DefaultPhotoLogRepository(
photologId: Long,
reaction: GoalReactionType,
): AppResult = safeApiCall { service.reactToPhotolog(photologId, ReactionRequest(reaction.toApi())) }
+
+ override suspend fun modifyPhotolog(
+ photologId: Long,
+ fileName: String,
+ comment: String,
+ ): AppResult = safeApiCall { service.modifyPhotolog(photologId, PhotologModifyRequest(fileName, comment)) }
}
diff --git a/domain/src/main/java/com/twix/domain/repository/PhotoLogRepository.kt b/domain/src/main/java/com/twix/domain/repository/PhotoLogRepository.kt
index bddd2607..063f5895 100644
--- a/domain/src/main/java/com/twix/domain/repository/PhotoLogRepository.kt
+++ b/domain/src/main/java/com/twix/domain/repository/PhotoLogRepository.kt
@@ -5,22 +5,29 @@ import com.twix.domain.model.photo.PhotoLogUploadInfo
import com.twix.domain.model.photo.PhotologParam
import com.twix.domain.model.photolog.PhotoLogs
import com.twix.result.AppResult
+import java.time.LocalDate
interface PhotoLogRepository {
suspend fun getUploadUrl(goalId: Long): AppResult
- suspend fun uploadPhotoLog(photologParam: PhotologParam): AppResult
+ suspend fun uploadPhotolog(photologParam: PhotologParam): AppResult
- suspend fun uploadPhotoLogImage(
+ suspend fun uploadPhotologImage(
goalId: Long,
bytes: ByteArray,
contentType: String,
): AppResult
- suspend fun fetchPhotoLogs(targetDate: String): AppResult
+ suspend fun fetchPhotologs(targetDate: LocalDate): AppResult
suspend fun reactToPhotolog(
photologId: Long,
reaction: GoalReactionType,
): AppResult
+
+ suspend fun modifyPhotolog(
+ photologId: Long,
+ fileName: String,
+ comment: String,
+ ): AppResult
}
diff --git a/feature/main/src/main/java/com/twix/home/HomeScreen.kt b/feature/main/src/main/java/com/twix/home/HomeScreen.kt
index 3958c234..4e4e5f1a 100644
--- a/feature/main/src/main/java/com/twix/home/HomeScreen.kt
+++ b/feature/main/src/main/java/com/twix/home/HomeScreen.kt
@@ -25,7 +25,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -71,7 +70,7 @@ fun HomeRoute(
navigateToGoalEditor: () -> Unit,
navigateToGoalManage: (LocalDate) -> Unit,
navigateToSettings: () -> Unit,
- navigateToCertification: (Long) -> Unit,
+ navigateToCertification: (Long, LocalDate) -> Unit,
navigateToCertificationDetail: (Long, LocalDate, BetweenUs) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -84,7 +83,7 @@ fun HomeRoute(
ActivityResultContracts.RequestPermission(),
) { granted ->
if (granted) {
- navigateToCertification(uiState.selectedGoalId)
+ navigateToCertification(uiState.selectedGoalId, uiState.selectedDate)
return@rememberLauncherForActivityResult
}
diff --git a/feature/main/src/main/java/com/twix/main/MainScreen.kt b/feature/main/src/main/java/com/twix/main/MainScreen.kt
index 119df5dc..fbf83b75 100644
--- a/feature/main/src/main/java/com/twix/main/MainScreen.kt
+++ b/feature/main/src/main/java/com/twix/main/MainScreen.kt
@@ -31,7 +31,7 @@ fun MainRoute(
navigateToGoalEditor: () -> Unit,
navigateToGoalManage: (LocalDate) -> Unit,
navigateToSettings: () -> Unit,
- navigateToCertification: (Long) -> Unit,
+ navigateToCertification: (Long, LocalDate) -> Unit,
navigateToCertificationDetail: (Long, LocalDate, BetweenUs) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -57,7 +57,7 @@ private fun MainScreen(
navigateToGoalEditor: () -> Unit,
navigateToGoalManage: (LocalDate) -> Unit,
navigateToSettings: () -> Unit,
- navigateToCertification: (Long) -> Unit,
+ navigateToCertification: (Long, LocalDate) -> Unit,
navigateToCertificationDetail: (Long, LocalDate, BetweenUs) -> Unit,
) {
val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle()
diff --git a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt
index 2ba9c31a..92a56514 100644
--- a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt
+++ b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt
@@ -6,6 +6,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.twix.main.MainRoute
import com.twix.navigation.NavRoutes
+import com.twix.navigation.args.DetailNavArgs
import com.twix.navigation.base.NavGraphContributor
object MainNavGraph : NavGraphContributor {
@@ -36,24 +37,27 @@ object MainNavGraph : NavGraphContributor {
launchSingleTop = true
}
},
- navigateToCertification = {
+ navigateToCertification = { goalId, date ->
val destination =
NavRoutes.TaskCertificationRoute.createRoute(
- goalId = it,
- from = NavRoutes.TaskCertificationRoute.From.HOME,
+ DetailNavArgs(
+ goalId = goalId,
+ from = NavRoutes.TaskCertificationRoute.From.HOME,
+ selectedDate = date.toString(),
+ ),
)
navController.navigate(destination) {
launchSingleTop = true
}
},
navigateToCertificationDetail = { goalId, date, betweenUs ->
- navController.navigate(
+ val destination =
NavRoutes.TaskCertificationDetailRoute.createRoute(
goalId,
date,
betweenUs.name,
- ),
- ) {
+ )
+ navController.navigate(destination) {
launchSingleTop = true
}
},
diff --git a/feature/task-certification/build.gradle.kts b/feature/task-certification/build.gradle.kts
index 4689af48..eb6da430 100644
--- a/feature/task-certification/build.gradle.kts
+++ b/feature/task-certification/build.gradle.kts
@@ -10,4 +10,5 @@ dependencies {
implementation(libs.bundles.cameraX)
implementation(libs.guava)
+ implementation(libs.kotlinx.serialization.json)
}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt
index 75d00aab..7f4bbfaa 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationScreen.kt
@@ -4,20 +4,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.ime
-import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -30,33 +22,29 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.twix.designsystem.components.comment.CIRCLE_SIZE
-import com.twix.designsystem.components.comment.CommentBox
+import com.twix.designsystem.R
+import com.twix.designsystem.components.comment.CommentAnchorFrame
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.components.toast.ToastManager
import com.twix.designsystem.components.toast.model.ToastData
-import com.twix.designsystem.theme.DimmedColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
-import com.twix.task_certification.R
-import com.twix.task_certification.certification.camera.Camera
import com.twix.task_certification.certification.component.CameraControlBar
import com.twix.task_certification.certification.component.CameraPreviewBox
import com.twix.task_certification.certification.component.CommentErrorText
import com.twix.task_certification.certification.component.TaskCertificationTopBar
-import com.twix.task_certification.certification.model.CameraPreview
-import com.twix.task_certification.certification.model.TaskCertificationIntent
-import com.twix.task_certification.certification.model.TaskCertificationSideEffect
-import com.twix.task_certification.certification.model.TaskCertificationUiState
+import com.twix.task_certification.certification.contract.TaskCertificationIntent
+import com.twix.task_certification.certification.contract.TaskCertificationSideEffect
+import com.twix.task_certification.certification.contract.TaskCertificationUiState
+import com.twix.task_certification.certification.model.camera.Camera
+import com.twix.task_certification.certification.model.camera.CameraPreview
import com.twix.ui.base.ObserveAsEvents
import com.twix.ui.extension.noRippleClickable
import kotlinx.coroutines.launch
@@ -69,6 +57,7 @@ fun TaskCertificationRoute(
camera: Camera = koinInject(),
viewModel: TaskCertificationViewModel = koinViewModel(),
navigateToBack: () -> Unit,
+ navigateToDetail: () -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val cameraPreview by camera.surfaceRequests.collectAsStateWithLifecycle()
@@ -106,8 +95,8 @@ fun TaskCertificationRoute(
),
)
}
-
- TaskCertificationSideEffect.NavigateToDetail -> navigateToBack()
+ TaskCertificationSideEffect.NavigateToBack -> navigateToBack()
+ TaskCertificationSideEffect.NavigateToDetail -> navigateToDetail()
}
}
@@ -165,27 +154,7 @@ private fun TaskCertificationScreen(
onFocusChanged: (Boolean) -> Unit,
) {
val focusManager = LocalFocusManager.current
-
var previewBoxBottom by remember { mutableFloatStateOf(0f) }
- val density = LocalDensity.current
- val imeBottom = WindowInsets.ime.getBottom(density)
-
- /**
- * Comment Circle UI의 높이
- * */
- val commentBoxHeight = with(density) { CIRCLE_SIZE.toPx() }
-
- /**
- * [기본 위치 설정]
- * 프리뷰 박스 하단(previewBoxBottom)을 기준으로 배치
- * circlePx * 2 만큼 위로 올리고 패딩(+20f)
- */
- val defaultY = previewBoxBottom - (commentBoxHeight * 2) + 20f
-
- /**
- * 키보드가 올라왔을 때 CommentBox와 키보드 사이의 최소 간격
- */
- val keyboardPadding = 60f
Box(
modifier =
@@ -194,11 +163,7 @@ private fun TaskCertificationScreen(
.background(GrayColor.C500)
.noRippleClickable { focusManager.clearFocus() },
) {
- Column(
- Modifier
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
TaskCertificationTopBar(onClickClose = onClickClose)
Spacer(modifier = Modifier.height(24.26.dp))
@@ -238,64 +203,12 @@ private fun TaskCertificationScreen(
)
}
- /**
- * 프리뷰 박스의 하단 좌표(previewBoxBottom)가 측정되었을 때만 렌더링 시작
- * */
- if (previewBoxBottom != 0f) {
- BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
- /**
- * 화면의 전체 높이와 키보드 상단 경계선 좌표 계산
- * */
- val screenHeight = constraints.maxHeight.toFloat()
- val keyboardTop = screenHeight - imeBottom
-
- /**
- * Dimmed 배경 레이어
- * 키보드가 올라왔을 때만 나타나도록 하며, 클릭 시 포커스를 해제
- */
- AnimatedVisibility(
- visible = uiState.commentUiModel.isFocused,
- enter = fadeIn(),
- exit = fadeOut(),
- ) {
- Box(
- modifier =
- Modifier
- .fillMaxSize()
- .background(DimmedColor.D070)
- .noRippleClickable { focusManager.clearFocus() },
- )
- }
-
- CommentBox(
- uiModel = uiState.commentUiModel,
- onCommentChanged = onCommentChanged,
- onFocusChanged = onFocusChanged,
- modifier =
- Modifier
- .fillMaxWidth()
- .offset {
- IntOffset(
- x = 0,
- y =
- if (imeBottom > 0 && (defaultY + commentBoxHeight) > keyboardTop) {
- /**
- * 키보드가 활성화되었고(imeBottom > 0),
- * 기존 위치(defaultY)가 키보드에 가려질 상황일 때만 실행됩니다.
- * imeBottom은 키보드가 닫히기 시작하면 실시간으로 줄어들어 0에 도달합니다.
- * 이때 if (imeBottom > 0) 조건이 false로 바뀌면서 즉시 else 문인 defaultY 위치로 점프하게 되는데,
- * 만약 시스템의 WindowInsets 애니메이션이 끝나기 전이나 레이아웃 재계산 중에 일시적으로 기준 좌표를 잃어
- * CommentBox가 화면 최하단을 갔다 원래 위치로 돌아오는 "튀는" 현상이 발생합니다.
- */
- (screenHeight - imeBottom - commentBoxHeight - keyboardPadding).toInt()
- } else {
- defaultY.toInt()
- },
- )
- },
- )
- }
- }
+ CommentAnchorFrame(
+ uiModel = uiState.comment,
+ anchorBottom = previewBoxBottom,
+ onCommentChanged = onCommentChanged,
+ onFocusChanged = onFocusChanged,
+ )
}
}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt
index 8730d13c..86c033bf 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/TaskCertificationViewModel.kt
@@ -3,15 +3,17 @@ package com.twix.task_certification.certification
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import com.twix.designsystem.R
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.domain.model.photo.PhotologParam
import com.twix.domain.repository.PhotoLogRepository
import com.twix.navigation.NavRoutes
-import com.twix.task_certification.R
+import com.twix.navigation.args.DetailNavArgs
+import com.twix.navigation.savedstate.decodeNavArgs
+import com.twix.task_certification.certification.contract.TaskCertificationIntent
+import com.twix.task_certification.certification.contract.TaskCertificationSideEffect
+import com.twix.task_certification.certification.contract.TaskCertificationUiState
import com.twix.task_certification.certification.model.CaptureStatus
-import com.twix.task_certification.certification.model.TaskCertificationIntent
-import com.twix.task_certification.certification.model.TaskCertificationSideEffect
-import com.twix.task_certification.certification.model.TaskCertificationUiState
import com.twix.ui.base.BaseViewModel
import com.twix.ui.image.ImageGenerator
import com.twix.util.bus.GoalRefreshBus
@@ -25,19 +27,20 @@ import java.time.LocalDate
class TaskCertificationViewModel(
private val imageGenerator: ImageGenerator,
private val photologRepository: PhotoLogRepository,
- private val taskCertificationRefreshBus: TaskCertificationRefreshBus,
+ private val detailRefreshBus: TaskCertificationRefreshBus,
private val goalRefreshBus: GoalRefreshBus,
- saveStateHandle: SavedStateHandle,
+ savedStateHandle: SavedStateHandle,
) : BaseViewModel(
TaskCertificationUiState(),
) {
- private val goalId: Long =
- saveStateHandle[NavRoutes.TaskCertificationRoute.ARG_GOAL_ID]
- ?: error(GOAL_ID_NOT_FOUND)
+ private val navArgs: DetailNavArgs =
+ savedStateHandle.decodeNavArgs(NavRoutes.TaskCertificationRoute.ARG_DATA)
- private val from: String =
- saveStateHandle[NavRoutes.TaskCertificationRoute.ARG_FROM]
- ?: error(FROM_NOT_FOUND)
+ init {
+ if (navArgs.from == NavRoutes.TaskCertificationRoute.From.EDITOR) {
+ reduceComment(navArgs.comment)
+ }
+ }
override suspend fun handleIntent(intent: TaskCertificationIntent) {
when (intent) {
@@ -93,7 +96,7 @@ class TaskCertificationViewModel(
private fun handleUploadIntent() {
val capture = currentState.capture as? CaptureStatus.Captured ?: return
- if (!currentState.commentUiModel.canUpload) {
+ if (!currentState.comment.canUpload) {
showValidationError()
return
}
@@ -113,7 +116,7 @@ class TaskCertificationViewModel(
private fun showValidationError() {
viewModelScope.launch {
- if (!currentState.commentUiModel.canUpload) {
+ if (!currentState.comment.canUpload) {
reduce { showCommentError() }
delay(ERROR_DISPLAY_DURATION_MS)
reduce { hideCommentError() }
@@ -124,40 +127,73 @@ class TaskCertificationViewModel(
private fun upload(image: ByteArray) {
launchResult(
block = {
- photologRepository.uploadPhotoLogImage(
- goalId = goalId,
+ photologRepository.uploadPhotologImage(
+ goalId = navArgs.goalId,
bytes = image,
contentType = "image/jpeg",
)
},
- onSuccess = { fileName -> uploadPhotoLog(fileName) },
+ onSuccess = { fileName ->
+ when (navArgs.from) {
+ NavRoutes.TaskCertificationRoute.From.DETAIL,
+ NavRoutes.TaskCertificationRoute.From.HOME,
+ -> uploadPhotolog(fileName)
+
+ NavRoutes.TaskCertificationRoute.From.EDITOR -> modifyPhotolog(fileName)
+ }
+ },
onError = {
showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
},
)
}
- private fun uploadPhotoLog(fileName: String) {
+ private fun uploadPhotolog(fileName: String) {
launchResult(
block = {
- photologRepository.uploadPhotoLog(
+ photologRepository.uploadPhotolog(
PhotologParam(
- goalId = goalId,
+ goalId = navArgs.goalId,
fileName = fileName,
- comment = currentState.commentUiModel.comment,
- verificationDate = LocalDate.now(),
+ comment = currentState.comment.value,
+ verificationDate = LocalDate.parse(navArgs.selectedDate),
),
)
},
+ onSuccess = { handleUploadPhotologSuccess() },
+ onError = {
+ showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
+ },
+ )
+ }
+
+ private fun handleUploadPhotologSuccess() {
+ when (navArgs.from) {
+ NavRoutes.TaskCertificationRoute.From.HOME ->
+ goalRefreshBus.notifyGoalListChanged()
+ NavRoutes.TaskCertificationRoute.From.DETAIL ->
+ detailRefreshBus.notifyChanged(TaskCertificationRefreshBus.Publisher.PHOTOLOG)
+ NavRoutes.TaskCertificationRoute.From.EDITOR -> Unit
+ }
+ tryEmitSideEffect(TaskCertificationSideEffect.NavigateToBack)
+ }
+
+ private fun modifyPhotolog(fileName: String) {
+ launchResult(
+ block = {
+ photologRepository.modifyPhotolog(
+ photologId = navArgs.photologId,
+ fileName = fileName,
+ comment = currentState.comment.value,
+ )
+ },
onSuccess = {
- when (NavRoutes.TaskCertificationRoute.From.valueOf(from)) {
- NavRoutes.TaskCertificationRoute.From.DETAIL -> taskCertificationRefreshBus.notifyChanged()
- NavRoutes.TaskCertificationRoute.From.HOME -> goalRefreshBus.notifyGoalListChanged()
- }
+ detailRefreshBus.notifyChanged(TaskCertificationRefreshBus.Publisher.PHOTOLOG)
tryEmitSideEffect(TaskCertificationSideEffect.NavigateToDetail)
},
onError = {
showToast(R.string.task_certification_upload_fail, ToastType.ERROR)
+ showToast(R.string.task_certification_modify_fail, ToastType.ERROR)
},
)
}
@@ -173,7 +209,5 @@ class TaskCertificationViewModel(
companion object {
private const val ERROR_DISPLAY_DURATION_MS = 1500L
- private const val GOAL_ID_NOT_FOUND = "Goal Id Argument Not Found"
- private const val FROM_NOT_FOUND = "From Argument Not Found"
}
}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt
index d76e1e8a..c8020dc3 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraControlBar.kt
@@ -25,12 +25,12 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
+import com.twix.designsystem.R
import com.twix.designsystem.components.button.AppRoundButton
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
-import com.twix.task_certification.R
import com.twix.task_certification.certification.model.CaptureStatus
import com.twix.ui.extension.noRippleClickable
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraPreviewBox.kt
index 637b6dfc..d97d5d57 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraPreviewBox.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CameraPreviewBox.kt
@@ -19,12 +19,12 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
+import com.twix.designsystem.R
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
-import com.twix.task_certification.R
-import com.twix.task_certification.certification.model.CameraPreview
import com.twix.task_certification.certification.model.CaptureStatus
import com.twix.task_certification.certification.model.TorchStatus
+import com.twix.task_certification.certification.model.camera.CameraPreview
import com.twix.ui.extension.noRippleClickable
@Composable
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CommentErrorText.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CommentErrorText.kt
index 685a016b..51bf8783 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CommentErrorText.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/CommentErrorText.kt
@@ -12,12 +12,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.twix.designsystem.R
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
-import com.twix.task_certification.R
@Composable
fun CommentErrorText(modifier: Modifier = Modifier) {
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt
index 87309324..e5b962d5 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/component/TaskCertificationTopBar.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -12,8 +13,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.twix.designsystem.R
import com.twix.designsystem.theme.GrayColor
-import com.twix.task_certification.R
import com.twix.ui.extension.noRippleClickable
@Composable
@@ -25,6 +26,7 @@ internal fun TaskCertificationTopBar(
modifier =
modifier
.fillMaxWidth()
+ .height(72.dp)
.background(color = GrayColor.C500),
) {
Image(
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationIntent.kt
similarity index 94%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationIntent.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationIntent.kt
index 23049957..cbf40808 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationIntent.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationIntent.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.certification.model
+package com.twix.task_certification.certification.contract
import android.net.Uri
import com.twix.ui.base.Intent
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationSideEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationSideEffect.kt
similarity index 73%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationSideEffect.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationSideEffect.kt
index 96cd4380..1d2b631f 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationSideEffect.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationSideEffect.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.certification.model
+package com.twix.task_certification.certification.contract
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.ui.base.SideEffect
@@ -9,5 +9,7 @@ sealed interface TaskCertificationSideEffect : SideEffect {
val type: ToastType,
) : TaskCertificationSideEffect
+ data object NavigateToBack : TaskCertificationSideEffect
+
data object NavigateToDetail : TaskCertificationSideEffect
}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt
similarity index 71%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationUiState.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt
index 7283fcc4..2e523cbd 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/TaskCertificationUiState.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/contract/TaskCertificationUiState.kt
@@ -1,9 +1,12 @@
-package com.twix.task_certification.certification.model
+package com.twix.task_certification.certification.contract
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.compose.runtime.Immutable
import com.twix.designsystem.components.comment.model.CommentUiModel
+import com.twix.task_certification.certification.model.CaptureStatus
+import com.twix.task_certification.certification.model.TorchStatus
+import com.twix.task_certification.certification.model.camera.CameraPreview
import com.twix.ui.base.State
@Immutable
@@ -12,11 +15,11 @@ data class TaskCertificationUiState(
val torch: TorchStatus = TorchStatus.Off,
val lens: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
val preview: CameraPreview? = null,
- val commentUiModel: CommentUiModel = CommentUiModel(),
+ val comment: CommentUiModel = CommentUiModel(),
val showCommentError: Boolean = false,
) : State {
val hasMaxCommentLength: Boolean
- get() = commentUiModel.hasMaxCommentLength
+ get() = comment.hasMaxCommentLength
val showTorch: Boolean
get() = capture is CaptureStatus.NotCaptured && lens == CameraSelector.DEFAULT_BACK_CAMERA
@@ -35,7 +38,7 @@ data class TaskCertificationUiState(
}
fun toggleTorch(): TaskCertificationUiState {
- val newFlashMode = TorchStatus.toggle(torch)
+ val newFlashMode = TorchStatus.Companion.toggle(torch)
return copy(torch = newFlashMode)
}
@@ -47,9 +50,9 @@ data class TaskCertificationUiState(
fun removePicture(): TaskCertificationUiState = copy(capture = CaptureStatus.NotCaptured)
- fun updateComment(comment: String) = copy(commentUiModel = commentUiModel.updateComment(comment))
+ fun updateComment(newComment: String) = copy(comment = comment.updateComment(newComment))
- fun updateCommentFocus(isFocused: Boolean) = copy(commentUiModel = commentUiModel.updateFocus(isFocused))
+ fun updateCommentFocus(isFocused: Boolean) = copy(comment = comment.updateFocus(isFocused))
fun showCommentError() = copy(showCommentError = true)
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/Camera.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/Camera.kt
similarity index 79%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/Camera.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/Camera.kt
index 51efc24a..bfd49e10 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/Camera.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/Camera.kt
@@ -1,9 +1,8 @@
-package com.twix.task_certification.certification.camera
+package com.twix.task_certification.certification.model.camera
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.lifecycle.LifecycleOwner
-import com.twix.task_certification.certification.model.CameraPreview
import com.twix.task_certification.certification.model.TorchStatus
import kotlinx.coroutines.flow.StateFlow
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/CameraPreview.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CameraPreview.kt
similarity index 71%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/model/CameraPreview.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CameraPreview.kt
index 17bea7a3..eca40459 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/CameraPreview.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CameraPreview.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.certification.model
+package com.twix.task_certification.certification.model.camera
import androidx.camera.core.SurfaceRequest
import androidx.compose.runtime.Immutable
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/CaptureCamera.kt b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt
similarity index 97%
rename from feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/CaptureCamera.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt
index 058f4bde..56f2a710 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/certification/camera/CaptureCamera.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/certification/model/camera/CaptureCamera.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.certification.camera
+package com.twix.task_certification.certification.model.camera
import android.content.ContentValues
import android.content.Context
@@ -15,7 +15,6 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
-import com.twix.task_certification.certification.model.CameraPreview
import com.twix.task_certification.certification.model.TorchStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
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 9431ceab..268d8ccf 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
@@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.twix.designsystem.R
import com.twix.designsystem.components.toast.ToastManager
import com.twix.designsystem.components.toast.model.ToastData
import com.twix.designsystem.components.toast.model.ToastType
@@ -28,12 +29,12 @@ import com.twix.designsystem.extension.showCameraPermissionToastWithNavigateToSe
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.GoalReactionType
-import com.twix.task_certification.detail.component.ReactionContent
import com.twix.task_certification.detail.component.TaskCertificationCardContent
import com.twix.task_certification.detail.component.TaskCertificationDetailTopBar
-import com.twix.task_certification.detail.model.TaskCertificationDetailIntent
-import com.twix.task_certification.detail.model.TaskCertificationDetailSideEffect
-import com.twix.task_certification.detail.model.TaskCertificationDetailUiState
+import com.twix.task_certification.detail.component.reaction.ReactionContent
+import com.twix.task_certification.detail.contract.TaskCertificationDetailIntent
+import com.twix.task_certification.detail.contract.TaskCertificationDetailSideEffect
+import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState
import com.twix.task_certification.detail.preview.TaskCertificationDetailPreviewProvider
import com.twix.ui.base.ObserveAsEvents
import com.twix.ui.extension.findActivity
@@ -41,13 +42,13 @@ import com.twix.ui.extension.hasCameraPermission
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
-import com.twix.designsystem.R as DesR
+import java.time.LocalDate
@Composable
fun TaskCertificationDetailRoute(
navigateToBack: () -> Unit,
- navigateToUpload: (Long) -> Unit,
- navigateToEditor: () -> Unit,
+ navigateToCertification: (Long, LocalDate) -> Unit,
+ navigateToEditor: (TaskCertificationDetailUiState) -> Unit,
toastManager: ToastManager = koinInject(),
viewModel: TaskCertificationDetailViewModel = koinViewModel(),
) {
@@ -72,7 +73,7 @@ fun TaskCertificationDetailRoute(
) { granted ->
if (granted) {
- navigateToUpload(uiState.goalId)
+ navigateToCertification(uiState.goalId, uiState.selectedDate)
return@rememberLauncherForActivityResult
}
val activity = currentContext.findActivity() ?: return@rememberLauncherForActivityResult
@@ -88,7 +89,7 @@ fun TaskCertificationDetailRoute(
toastManager.show(
ToastData(
currentContext.getString(
- DesR.string.toast_camera_permission_request,
+ R.string.toast_camera_permission_request,
),
ToastType.ERROR,
),
@@ -100,11 +101,11 @@ fun TaskCertificationDetailRoute(
TaskCertificationDetailScreen(
uiState = uiState,
onBack = navigateToBack,
- onClickModify = { navigateToEditor() },
+ onClickModify = { navigateToEditor(uiState) },
onClickReaction = { viewModel.dispatch(TaskCertificationDetailIntent.Reaction(it)) },
onClickUpload = {
if (currentContext.hasCameraPermission()) {
- navigateToUpload(uiState.goalId)
+ navigateToCertification(uiState.goalId, uiState.selectedDate)
} else {
permissionLauncher.launch(Manifest.permission.CAMERA)
}
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 1907c32c..e3cb4d09 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
@@ -2,16 +2,16 @@ package com.twix.task_certification.detail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import com.twix.designsystem.R
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.navigation.NavRoutes
-import com.twix.task_certification.R
-import com.twix.task_certification.detail.model.TaskCertificationDetailIntent
-import com.twix.task_certification.detail.model.TaskCertificationDetailSideEffect
-import com.twix.task_certification.detail.model.TaskCertificationDetailUiState
-import com.twix.task_certification.detail.model.toUiState
+import com.twix.task_certification.detail.contract.TaskCertificationDetailIntent
+import com.twix.task_certification.detail.contract.TaskCertificationDetailSideEffect
+import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState
+import com.twix.task_certification.detail.contract.toUiState
import com.twix.ui.base.BaseViewModel
import com.twix.util.bus.GoalRefreshBus
import com.twix.util.bus.TaskCertificationRefreshBus
@@ -22,10 +22,11 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
+import java.time.LocalDate
class TaskCertificationDetailViewModel(
private val photologRepository: PhotoLogRepository,
- private val taskCertificationRefreshBus: TaskCertificationRefreshBus,
+ private val detailRefreshBus: TaskCertificationRefreshBus,
private val goalRefreshBus: GoalRefreshBus,
savedStateHandle: SavedStateHandle,
) : BaseViewModel(
@@ -35,9 +36,11 @@ class TaskCertificationDetailViewModel(
savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_GOAL_ID]
?: error(GOAL_ID_NOT_FOUND)
- private val argTargetDate: String =
- savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_DATE]
- ?: error(TARGET_DATE_NOT_FOUND)
+ private val argTargetDate: LocalDate =
+ LocalDate.parse(
+ savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_DATE]
+ ?: error(TARGET_DATE_NOT_FOUND),
+ )
private val argBetweenUs: String =
savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_BETWEEN_US]
@@ -59,8 +62,8 @@ class TaskCertificationDetailViewModel(
private fun fetchPhotolog() {
launchResult(
- block = { photologRepository.fetchPhotoLogs(argTargetDate) },
- onSuccess = { reduce { it.toUiState(argGoalId, argBetweenUs) } },
+ block = { photologRepository.fetchPhotologs(argTargetDate) },
+ onSuccess = { reduce { it.toUiState(argGoalId, argBetweenUs, argTargetDate) } },
onError = {
showToast(R.string.task_certification_detail_fetch_photolog_fail, ToastType.ERROR)
},
@@ -98,17 +101,17 @@ class TaskCertificationDetailViewModel(
}
}
- private suspend fun reduceReaction(reaction: GoalReactionType) {
- lastReaction = currentState.partnerPhotolog?.reaction
- reduce { currentState.copy(partnerPhotolog = partnerPhotolog?.updateReaction(reaction)) }
- reactionFlow.emit(reaction)
- }
-
private fun collectEventBus() {
viewModelScope.launch {
- taskCertificationRefreshBus.events.collect {
- fetchPhotolog()
- goalRefreshBus.notifyGoalListChanged()
+ detailRefreshBus.events.collect { publisher ->
+ when (publisher) {
+ TaskCertificationRefreshBus.Publisher.PHOTOLOG -> {
+ fetchPhotolog()
+ goalRefreshBus.notifyGoalListChanged()
+ }
+
+ TaskCertificationRefreshBus.Publisher.EDITOR -> fetchPhotolog()
+ }
}
}
}
@@ -121,6 +124,12 @@ class TaskCertificationDetailViewModel(
}
}
+ private suspend fun reduceReaction(reaction: GoalReactionType) {
+ lastReaction = currentState.partnerPhotolog?.reaction
+ reduce { currentState.copy(partnerPhotolog = partnerPhotolog?.updateReaction(reaction)) }
+ reactionFlow.emit(reaction)
+ }
+
private fun reduceShownCard() {
reduce { toggleBetweenUs() }
}
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 6f0c2a15..d61b2395 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
@@ -5,10 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import com.twix.designsystem.R
+import com.twix.designsystem.components.photolog.BackgroundCard
+import com.twix.designsystem.components.photolog.ForegroundCard
import com.twix.domain.model.enums.BetweenUs
-import com.twix.task_certification.R
-import com.twix.task_certification.detail.model.TaskCertificationDetailUiState
-import com.twix.task_certification.detail.swipe.SwipeableCard
+import com.twix.task_certification.detail.component.swipe.SwipeableCard
+import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState
@Composable
internal fun TaskCertificationCardContent(
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt
index ee916be2..4ee17ab4 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationDetailTopBar.kt
@@ -14,6 +14,7 @@ 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 com.twix.designsystem.R
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.components.topbar.CommonTopBar
import com.twix.designsystem.theme.CommonColor
@@ -21,7 +22,6 @@ import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
import com.twix.ui.extension.noRippleClickable
-import com.twix.designsystem.R as DesR
@Composable
internal fun TaskCertificationDetailTopBar(
@@ -34,7 +34,7 @@ internal fun TaskCertificationDetailTopBar(
title = title,
left = {
Image(
- painter = painterResource(DesR.drawable.ic_arrow3_left),
+ painter = painterResource(R.drawable.ic_arrow3_left),
contentDescription = "back",
modifier =
Modifier
@@ -54,7 +54,7 @@ internal fun TaskCertificationDetailTopBar(
contentAlignment = Alignment.Center,
) {
AppText(
- text = stringResource(DesR.string.word_modify),
+ text = stringResource(R.string.word_modify),
style = AppTextStyle.T2,
color = GrayColor.C500,
)
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionBar.kt
similarity index 98%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionBar.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionBar.kt
index 58d068a8..cfdcf082 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionBar.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionBar.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.reaction
+package com.twix.task_certification.detail.component.reaction
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ReactionContent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionContent.kt
similarity index 86%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ReactionContent.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionContent.kt
index 1081c5a1..75da1cad 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/ReactionContent.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionContent.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.component
+package com.twix.task_certification.detail.component.reaction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -13,9 +13,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.GoalReactionType
-import com.twix.task_certification.detail.reaction.ReactionBar
-import com.twix.task_certification.detail.reaction.ReactionEffect
-import com.twix.task_certification.detail.reaction.ReactionUiModel
@Composable
internal fun ReactionContent(
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt
similarity index 99%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffect.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt
index de5bbba7..696eb339 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffect.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.reaction
+package com.twix.task_certification.detail.component.reaction
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffectSpec.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffectSpec.kt
similarity index 91%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffectSpec.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffectSpec.kt
index 3f1db381..455b1fdf 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionEffectSpec.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffectSpec.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.reaction
+package com.twix.task_certification.detail.component.reaction
/**
* ReactionEffect 애니메이션 설정 값 묶음.
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionParticle.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionParticle.kt
similarity index 87%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionParticle.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionParticle.kt
index 3b4c3dc2..43b72f30 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionParticle.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionParticle.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.reaction
+package com.twix.task_certification.detail.component.reaction
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionUiModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt
similarity index 92%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionUiModel.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt
index fd0dc409..6f68fd1a 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/reaction/ReactionUiModel.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.reaction
+package com.twix.task_certification.detail.component.reaction
import com.twix.designsystem.R
import com.twix.domain.model.enums.GoalReactionType
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeCardSpec.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeCardSpec.kt
similarity index 92%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeCardSpec.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeCardSpec.kt
index 7ef4a845..54a445bd 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeCardSpec.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeCardSpec.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.swipe
+package com.twix.task_certification.detail.component.swipe
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeableCard.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt
similarity index 99%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeableCard.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt
index 7e42fefe..9c86d778 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/swipe/SwipeableCard.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/swipe/SwipeableCard.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.swipe
+package com.twix.task_certification.detail.component.swipe
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt
similarity index 87%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailIntent.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt
index f87884b3..650100dd 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailIntent.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.model
+package com.twix.task_certification.detail.contract
import com.twix.domain.model.enums.GoalReactionType
import com.twix.ui.base.Intent
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailSideEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt
similarity index 84%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailSideEffect.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt
index b603e2f4..202348ef 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailSideEffect.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt
@@ -1,4 +1,4 @@
-package com.twix.task_certification.detail.model
+package com.twix.task_certification.detail.contract
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.ui.base.SideEffect
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt
similarity index 84%
rename from feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailUiState.kt
rename to feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt
index ac220851..8dd2d896 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/model/TaskCertificationDetailUiState.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt
@@ -1,17 +1,20 @@
-package com.twix.task_certification.detail.model
+package com.twix.task_certification.detail.contract
import androidx.compose.runtime.Immutable
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.navigation.args.EditorNavArgs
import com.twix.ui.base.State
import com.twix.util.RelativeTimeFormatter
+import java.time.LocalDate
@Immutable
data class TaskCertificationDetailUiState(
val goalId: Long = -1L,
val currentShow: BetweenUs = BetweenUs.PARTNER,
+ val selectedDate: LocalDate = LocalDate.now(),
val myNickname: String = "",
val partnerNickname: String = "",
val goalName: String = "",
@@ -70,11 +73,22 @@ data class TaskCertificationDetailUiState(
val canReaction: Boolean
get() =
currentShow == BetweenUs.PARTNER && isDisplayedGoalCertificated
+
+ fun toSerializer() =
+ EditorNavArgs(
+ goalId = goalId,
+ nickname = myNickname,
+ goalName = goalName,
+ photologId = myPhotolog?.photologId ?: -1,
+ imageUrl = myPhotolog?.imageUrl ?: "",
+ comment = myPhotolog?.comment,
+ )
}
fun PhotoLogs.toUiState(
goalId: Long,
betweenUs: String,
+ selectedDate: LocalDate,
): TaskCertificationDetailUiState {
val currentGoalPhotolog =
goals.firstOrNull {
@@ -84,6 +98,7 @@ fun PhotoLogs.toUiState(
return TaskCertificationDetailUiState(
goalId = goalId,
currentShow = BetweenUs.valueOf(betweenUs),
+ selectedDate = selectedDate,
myNickname = myNickname,
partnerNickname = partnerNickname,
goalName = currentGoalPhotolog.goalName,
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/preview/TaskCertificationDetailPreviewProvider.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/preview/TaskCertificationDetailPreviewProvider.kt
index a678d2cf..03eebdbf 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/preview/TaskCertificationDetailPreviewProvider.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/preview/TaskCertificationDetailPreviewProvider.kt
@@ -5,7 +5,7 @@ import com.twix.domain.model.enums.BetweenUs
import com.twix.domain.model.enums.GoalIconType
import com.twix.domain.model.enums.GoalReactionType
import com.twix.domain.model.photolog.PhotologDetail
-import com.twix.task_certification.detail.model.TaskCertificationDetailUiState
+import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState
class TaskCertificationDetailPreviewProvider : PreviewParameterProvider {
override val values =
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/di/TaskCertificationModule.kt b/feature/task-certification/src/main/java/com/twix/task_certification/di/TaskCertificationModule.kt
index 6d221630..fddd2a5a 100644
--- a/feature/task-certification/src/main/java/com/twix/task_certification/di/TaskCertificationModule.kt
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/di/TaskCertificationModule.kt
@@ -3,9 +3,10 @@ package com.twix.task_certification.di
import com.twix.navigation.NavRoutes
import com.twix.navigation.base.NavGraphContributor
import com.twix.task_certification.certification.TaskCertificationViewModel
-import com.twix.task_certification.certification.camera.Camera
-import com.twix.task_certification.certification.camera.CaptureCamera
+import com.twix.task_certification.certification.model.camera.Camera
+import com.twix.task_certification.certification.model.camera.CaptureCamera
import com.twix.task_certification.detail.TaskCertificationDetailViewModel
+import com.twix.task_certification.editor.TaskCertificationEditorViewModel
import com.twix.task_certification.navigation.TaskCertificationGraph
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.named
@@ -15,6 +16,7 @@ val taskCertificationModule =
module {
viewModelOf(::TaskCertificationDetailViewModel)
viewModelOf(::TaskCertificationViewModel)
+ viewModelOf(::TaskCertificationEditorViewModel)
factory { CaptureCamera(get()) }
single(named(NavRoutes.TaskCertificationRoute.route)) { TaskCertificationGraph }
}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt
new file mode 100644
index 00000000..29f3d33c
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorRoute.kt
@@ -0,0 +1,206 @@
+package com.twix.task_certification.editor
+
+import android.Manifest
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.app.ActivityCompat
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coil3.compose.AsyncImage
+import coil3.request.ImageRequest
+import coil3.request.crossfade
+import com.twix.designsystem.R
+import com.twix.designsystem.components.comment.CommentAnchorFrame
+import com.twix.designsystem.components.photolog.PhotologCard
+import com.twix.designsystem.components.toast.ToastManager
+import com.twix.designsystem.components.toast.model.ToastData
+import com.twix.designsystem.components.toast.model.ToastType
+import com.twix.designsystem.extension.showCameraPermissionToastWithNavigateToSettingAction
+import com.twix.designsystem.theme.CommonColor
+import com.twix.designsystem.theme.TwixTheme
+import com.twix.task_certification.editor.component.RetakeButton
+import com.twix.task_certification.editor.component.TaskCertificationEditorTopBar
+import com.twix.task_certification.editor.contract.TaskCertificationEditorIntent
+import com.twix.task_certification.editor.contract.TaskCertificationEditorSideEffect
+import com.twix.task_certification.editor.contract.TaskCertificationEditorUiState
+import com.twix.ui.base.ObserveAsEvents
+import com.twix.ui.extension.findActivity
+import com.twix.ui.extension.hasCameraPermission
+import com.twix.ui.extension.noRippleClickable
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
+
+@Composable
+fun TaskCertificationEditorRoute(
+ navigateToBack: () -> Unit,
+ navigateToCertification: (Long, Long, String) -> Unit,
+ toastManager: ToastManager = koinInject(),
+ viewModel: TaskCertificationEditorViewModel = koinViewModel(),
+) {
+ val context = LocalContext.current
+ val currentContext by rememberUpdatedState(context)
+ val coroutineScope = rememberCoroutineScope()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ ObserveAsEvents(viewModel.sideEffect) { sideEffect ->
+ when (sideEffect) {
+ is TaskCertificationEditorSideEffect.ShowToast ->
+ toastManager.tryShow(
+ ToastData(
+ currentContext.getString(sideEffect.message),
+ sideEffect.type,
+ ),
+ )
+ }
+ }
+
+ val permissionLauncher =
+ rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission(),
+ ) { granted ->
+
+ if (granted) {
+ navigateToCertification(uiState.goalId, uiState.photologId, uiState.comment.value)
+ return@rememberLauncherForActivityResult
+ }
+ val activity = currentContext.findActivity() ?: return@rememberLauncherForActivityResult
+ val shouldShowRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(
+ activity,
+ Manifest.permission.CAMERA,
+ )
+ coroutineScope.launch {
+ if (!shouldShowRationale) {
+ toastManager.showCameraPermissionToastWithNavigateToSettingAction(currentContext)
+ } else {
+ toastManager.show(
+ ToastData(
+ currentContext.getString(
+ R.string.toast_camera_permission_request,
+ ),
+ ToastType.ERROR,
+ ),
+ )
+ }
+ }
+ }
+
+ TaskCertificationEditorScreen(
+ uiState = uiState,
+ onBack = navigateToBack,
+ onClickSave = { viewModel.dispatch(TaskCertificationEditorIntent.Save) },
+ onFocusChanged = { viewModel.dispatch(TaskCertificationEditorIntent.CommentFocusChanged(it)) },
+ onCommentChanged = { viewModel.dispatch(TaskCertificationEditorIntent.ModifyComment(it)) },
+ onClickRetake = {
+ if (currentContext.hasCameraPermission()) {
+ navigateToCertification(uiState.goalId, uiState.photologId, uiState.comment.value)
+ } else {
+ permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ },
+ )
+}
+
+@Composable
+fun TaskCertificationEditorScreen(
+ uiState: TaskCertificationEditorUiState,
+ onBack: () -> Unit,
+ onClickSave: () -> Unit,
+ onCommentChanged: (String) -> Unit,
+ onFocusChanged: (Boolean) -> Unit,
+ onClickRetake: () -> Unit,
+) {
+ val focusManager = LocalFocusManager.current
+ var photologBottom by remember { mutableFloatStateOf(0f) }
+
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .background(color = CommonColor.White)
+ .noRippleClickable { focusManager.clearFocus() },
+ ) {
+ Column {
+ TaskCertificationEditorTopBar(
+ title = uiState.goalName,
+ onBack = onBack,
+ onClickSave = onClickSave,
+ )
+
+ Spacer(Modifier.height(103.dp))
+
+ PhotologCard(
+ modifier =
+ Modifier
+ .onGloballyPositioned { coordinates ->
+ val bottom = coordinates.boundsInParent().bottom
+ if (photologBottom != bottom) {
+ photologBottom = bottom
+ }
+ },
+ ) {
+ AsyncImage(
+ model =
+ ImageRequest
+ .Builder(LocalContext.current)
+ .data(uiState.imageUrl)
+ .crossfade(true)
+ .build(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+ }
+
+ Spacer(Modifier.height(101.dp))
+
+ RetakeButton(onClickRetake = onClickRetake)
+ }
+
+ CommentAnchorFrame(
+ uiModel = uiState.comment,
+ anchorBottom = photologBottom,
+ onCommentChanged = onCommentChanged,
+ onFocusChanged = onFocusChanged,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun TaskCertificationEditorScreenPreview() {
+ TwixTheme {
+ TaskCertificationEditorScreen(
+ uiState =
+ TaskCertificationEditorUiState(
+ nickname = "페토",
+ goalName = "아이스크림 먹기",
+ ),
+ onBack = {},
+ onClickSave = {},
+ onFocusChanged = {},
+ onClickRetake = {},
+ onCommentChanged = {},
+ )
+ }
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt
new file mode 100644
index 00000000..0cf58c24
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/TaskCertificationEditorViewModel.kt
@@ -0,0 +1,86 @@
+package com.twix.task_certification.editor
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.twix.designsystem.R
+import com.twix.designsystem.components.toast.model.ToastType
+import com.twix.domain.repository.PhotoLogRepository
+import com.twix.navigation.NavRoutes
+import com.twix.navigation.args.EditorNavArgs
+import com.twix.navigation.savedstate.decodeNavArgs
+import com.twix.result.AppResult
+import com.twix.task_certification.editor.contract.TaskCertificationEditorIntent
+import com.twix.task_certification.editor.contract.TaskCertificationEditorSideEffect
+import com.twix.task_certification.editor.contract.TaskCertificationEditorUiState
+import com.twix.task_certification.editor.contract.toUiState
+import com.twix.ui.base.BaseViewModel
+import com.twix.util.bus.TaskCertificationRefreshBus
+import kotlinx.coroutines.launch
+
+class TaskCertificationEditorViewModel(
+ private val photologRepository: PhotoLogRepository,
+ private val detailRefreshBus: TaskCertificationRefreshBus,
+ savedStateHandle: SavedStateHandle,
+) : BaseViewModel(
+ TaskCertificationEditorUiState(),
+ ) {
+ private val navArgs: EditorNavArgs =
+ savedStateHandle.decodeNavArgs(NavRoutes.TaskCertificationEditorRoute.ARG_DATA)
+
+ init {
+ reduce { navArgs.toUiState() }
+ }
+
+ override suspend fun handleIntent(intent: TaskCertificationEditorIntent) {
+ when (intent) {
+ is TaskCertificationEditorIntent.CommentFocusChanged -> reduceCommentFocus(intent.isFocused)
+ is TaskCertificationEditorIntent.ModifyComment -> reduceComment(intent.value)
+ TaskCertificationEditorIntent.Save -> modifyComment()
+ }
+ }
+
+ private fun reduceCommentFocus(value: Boolean) {
+ reduce { copy(comment = comment.copy(isFocused = value)) }
+ }
+
+ private fun reduceComment(value: String) {
+ reduce { copy(comment = comment.copy(value = value)) }
+ }
+
+ private fun modifyComment() {
+ if (currentState.comment.canUpload.not()) {
+ showToast(R.string.comment_error_message, ToastType.ERROR)
+ } else if (currentState.isCommentNotChanged) {
+ showToast(R.string.task_certification_editor_not_modified, ToastType.ERROR)
+ } else {
+ launchResult(
+ block = { launchModifyComment() },
+ onSuccess = {
+ detailRefreshBus.notifyChanged(TaskCertificationRefreshBus.Publisher.EDITOR)
+ showToast(R.string.task_certification_editor_modify_success, ToastType.SUCCESS)
+ },
+ onError = {
+ showToast(R.string.task_certification_editor_modify_fail, ToastType.ERROR)
+ },
+ )
+ }
+ }
+
+ private fun showToast(
+ message: Int,
+ type: ToastType,
+ ) {
+ viewModelScope.launch {
+ emitSideEffect(
+ TaskCertificationEditorSideEffect.ShowToast(message, type),
+ )
+ }
+ }
+
+ private suspend fun launchModifyComment(): AppResult =
+ photologRepository.modifyPhotolog(
+ currentState.photologId,
+ currentState.imageName,
+ currentState.comment.value,
+ )
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/RetakeButton.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/RetakeButton.kt
new file mode 100644
index 00000000..c55390cb
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/RetakeButton.kt
@@ -0,0 +1,42 @@
+package com.twix.task_certification.editor.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.twix.designsystem.R
+import com.twix.designsystem.components.button.AppRoundButton
+import com.twix.designsystem.theme.CommonColor
+import com.twix.designsystem.theme.GrayColor
+import com.twix.designsystem.theme.TwixTheme
+import com.twix.ui.extension.noRippleClickable
+
+@Composable
+internal fun RetakeButton(
+ onClickRetake: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AppRoundButton(
+ text = stringResource(R.string.task_certification_editor_retake),
+ textColor = GrayColor.C500,
+ backgroundColor = CommonColor.White,
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .height(68.dp)
+ .padding(horizontal = 30.dp)
+ .noRippleClickable { onClickRetake() },
+ )
+}
+
+@Preview
+@Composable
+private fun RetakeButtonPreview() {
+ TwixTheme {
+ RetakeButton(onClickRetake = {})
+ }
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/TaskCertificationEditorTopBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/TaskCertificationEditorTopBar.kt
new file mode 100644
index 00000000..bd3c2084
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/component/TaskCertificationEditorTopBar.kt
@@ -0,0 +1,83 @@
+package com.twix.task_certification.editor.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.unit.dp
+import com.twix.designsystem.R
+import com.twix.designsystem.components.text.AppText
+import com.twix.designsystem.components.topbar.CommonTopBar
+import com.twix.designsystem.theme.CommonColor
+import com.twix.designsystem.theme.GrayColor
+import com.twix.designsystem.theme.TwixTheme
+import com.twix.domain.model.enums.AppTextStyle
+import com.twix.ui.extension.noRippleClickable
+
+@Composable
+internal fun TaskCertificationEditorTopBar(
+ title: String,
+ onBack: () -> Unit,
+ onClickSave: () -> Unit,
+) {
+ CommonTopBar(
+ title = title,
+ left = {
+ Image(
+ painter = painterResource(R.drawable.ic_arrow3_left),
+ contentDescription = "back",
+ modifier =
+ Modifier
+ .padding(18.dp)
+ .size(24.dp)
+ .noRippleClickable(onClick = onBack),
+ )
+ },
+ right = {
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .background(GrayColor.C100)
+ .noRippleClickable { onClickSave() },
+ contentAlignment = Alignment.Center,
+ ) {
+ AppText(
+ text = stringResource(R.string.word_save),
+ style = AppTextStyle.T2,
+ color = GrayColor.C500,
+ )
+ }
+ },
+ modifier = Modifier.background(color = CommonColor.White),
+ )
+}
+
+@Preview
+@Composable
+fun TaskCertificationEditorTopBarPreview() {
+ TwixTheme {
+ Column {
+ TaskCertificationEditorTopBar(
+ title = "목표 인증",
+ onBack = {},
+ onClickSave = {},
+ )
+
+ TaskCertificationEditorTopBar(
+ title = "목표 인증",
+ onBack = {},
+ onClickSave = {},
+ )
+ }
+ }
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorIntent.kt
new file mode 100644
index 00000000..42bf9693
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorIntent.kt
@@ -0,0 +1,15 @@
+package com.twix.task_certification.editor.contract
+
+import com.twix.ui.base.Intent
+
+sealed interface TaskCertificationEditorIntent : Intent {
+ data object Save : TaskCertificationEditorIntent
+
+ data class CommentFocusChanged(
+ val isFocused: Boolean,
+ ) : TaskCertificationEditorIntent
+
+ data class ModifyComment(
+ val value: String,
+ ) : TaskCertificationEditorIntent
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorSideEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorSideEffect.kt
new file mode 100644
index 00000000..f46c5660
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorSideEffect.kt
@@ -0,0 +1,11 @@
+package com.twix.task_certification.editor.contract
+
+import com.twix.designsystem.components.toast.model.ToastType
+import com.twix.ui.base.SideEffect
+
+sealed interface TaskCertificationEditorSideEffect : SideEffect {
+ data class ShowToast(
+ val message: Int,
+ val type: ToastType,
+ ) : TaskCertificationEditorSideEffect
+}
diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorUiState.kt
new file mode 100644
index 00000000..13a217ea
--- /dev/null
+++ b/feature/task-certification/src/main/java/com/twix/task_certification/editor/contract/TaskCertificationEditorUiState.kt
@@ -0,0 +1,38 @@
+package com.twix.task_certification.editor.contract
+
+import androidx.compose.runtime.Immutable
+import com.twix.designsystem.components.comment.model.CommentUiModel
+import com.twix.navigation.args.EditorNavArgs
+import com.twix.ui.base.State
+
+@Immutable
+data class TaskCertificationEditorUiState(
+ val goalId: Long = -1,
+ val photologId: Long = -1,
+ val nickname: String = "",
+ val goalName: String = "",
+ val imageUrl: String = "",
+ val comment: CommentUiModel = CommentUiModel(),
+ val originComment: String = "",
+) : State {
+ val isCommentNotChanged: Boolean
+ get() = comment.value == originComment
+
+ val imageName: String
+ get() = imageUrl.split(IMAGE_NAME_SEPARATOR).last()
+
+ companion object {
+ private const val IMAGE_NAME_SEPARATOR = "/"
+ }
+}
+
+internal fun EditorNavArgs.toUiState() =
+ TaskCertificationEditorUiState(
+ goalId = goalId,
+ nickname = nickname,
+ goalName = goalName,
+ photologId = photologId,
+ imageUrl = imageUrl,
+ comment = CommentUiModel(comment.orEmpty()),
+ originComment = comment.orEmpty(),
+ )
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 c80db656..ba37cbd8 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
@@ -7,9 +7,11 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navigation
import com.twix.navigation.NavRoutes
+import com.twix.navigation.args.DetailNavArgs
import com.twix.navigation.base.NavGraphContributor
import com.twix.task_certification.certification.TaskCertificationRoute
import com.twix.task_certification.detail.TaskCertificationDetailRoute
+import com.twix.task_certification.editor.TaskCertificationEditorRoute
object TaskCertificationGraph : NavGraphContributor {
override val graphRoute: NavRoutes
@@ -35,22 +37,55 @@ object TaskCertificationGraph : NavGraphContributor {
navArgument(NavRoutes.TaskCertificationDetailRoute.ARG_BETWEEN_US) {
type = NavType.StringType
},
- navArgument(NavRoutes.TaskCertificationDetailRoute.ARG_DATE) {
+ ),
+ ) {
+ TaskCertificationDetailRoute(
+ navigateToBack = navController::popBackStack,
+ navigateToCertification = { goalId, date ->
+ val destination =
+ NavRoutes.TaskCertificationRoute.createRoute(
+ DetailNavArgs(
+ goalId = goalId,
+ from = NavRoutes.TaskCertificationRoute.From.DETAIL,
+ selectedDate = date.toString(),
+ ),
+ )
+ navController.navigate(destination)
+ },
+ navigateToEditor = { uiState ->
+ val serializer = uiState.toSerializer()
+ navController.navigate(
+ NavRoutes.TaskCertificationEditorRoute.createRoute(
+ serializer,
+ ),
+ )
+ },
+ )
+ }
+
+ composable(
+ route = NavRoutes.TaskCertificationEditorRoute.route,
+ arguments =
+ listOf(
+ navArgument(NavRoutes.TaskCertificationEditorRoute.ARG_DATA) {
type = NavType.StringType
},
),
) {
- TaskCertificationDetailRoute(
+ TaskCertificationEditorRoute(
navigateToBack = navController::popBackStack,
- navigateToUpload = {
+ navigateToCertification = { goalId, photologId, comment ->
val destination =
NavRoutes.TaskCertificationRoute.createRoute(
- goalId = it,
- from = NavRoutes.TaskCertificationRoute.From.DETAIL,
+ DetailNavArgs(
+ goalId = goalId,
+ from = NavRoutes.TaskCertificationRoute.From.EDITOR,
+ photologId = photologId,
+ comment = comment,
+ ),
)
navController.navigate(destination)
},
- navigateToEditor = { },
)
}
@@ -58,17 +93,18 @@ object TaskCertificationGraph : NavGraphContributor {
route = NavRoutes.TaskCertificationRoute.route,
arguments =
listOf(
- navArgument(NavRoutes.TaskCertificationRoute.ARG_GOAL_ID) {
- type = NavType.LongType
- },
- navArgument(NavRoutes.TaskCertificationRoute.ARG_FROM) {
+ navArgument(NavRoutes.TaskCertificationRoute.ARG_DATA) {
type = NavType.StringType
},
),
) {
TaskCertificationRoute(
- navigateToBack = {
- navController.popBackStack()
+ navigateToBack = navController::popBackStack,
+ navigateToDetail = {
+ navController.popBackStack(
+ route = NavRoutes.TaskCertificationDetailRoute.route,
+ inclusive = false,
+ )
},
)
}
diff --git a/feature/task-certification/src/main/res/values/strings.xml b/feature/task-certification/src/main/res/values/strings.xml
deleted file mode 100644
index 4cc8b8da..00000000
--- a/feature/task-certification/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
- 업로드
- 이미지 캡처에 실패했습니다. 다시 시도해 주세요.
- 이미지를 불러오는 데 실패했습니다. 다시 시도해 주세요.
- 인증샷 찍기
- 인증샷 등록에 실패했습니다.
- 이미지 변환에 실패했습니다.
- 코멘트는 5글자로 입력해주세요!
-
-
- %s님은\n아직…
- 인증샷 조회에 실패했습니다.
- 찌르기
- 리액션 요청에 실패했어요.
-
- 인증샷 촬영을 위해서 카메라 권한이 필요해요.
- 설정
-