From 303927bff1cbf96e139be310a2d9ae0dbb776ac0 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 13:06:20 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=9B=90=ED=98=95=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/comment/CommentCircle.kt | 61 ++++++++ .../components/comment/CommentTextField.kt | 135 ++++++++++++++++++ .../comment/model/CommentUiModel.kt | 44 ++++++ .../src/main/res/values/strings.xml | 6 +- 4 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentCircle.kt create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentCircle.kt b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentCircle.kt new file mode 100644 index 0000000..bcb9d2c --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentCircle.kt @@ -0,0 +1,61 @@ +package com.twix.designsystem.components.comment + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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 + +@Composable +internal fun CommentCircle( + text: String, + showPlaceholder: Boolean, + showCursor: Boolean, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .size(64.dp) + .background(color = CommonColor.White, shape = CircleShape), + ) { + AppText( + text = text, + style = AppTextStyle.H1, + color = if (showPlaceholder) GrayColor.C200 else GrayColor.C500, + ) + + if (showCursor) CursorBar() + } +} + +@Composable +private fun CursorBar() { + Box( + modifier = + Modifier + .width(2.dp) + .height(28.dp) + .background(GrayColor.C500), + ) +} + +@Preview +@Composable +private fun CommentCirclePreview() { + TwixTheme { + CommentCircle(text = "1", showPlaceholder = false, showCursor = false) + } +} 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 new file mode 100644 index 0000000..9afed87 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentTextField.kt @@ -0,0 +1,135 @@ +package com.twix.designsystem.components.comment + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawWithCache +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.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +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 +import com.twix.designsystem.R +import com.twix.designsystem.components.comment.model.CommentUiModel +import com.twix.designsystem.theme.TwixTheme + +private val CIRCLE_PADDING_START: Dp = 50.dp +private val CIRCLE_SIZE: Dp = 64.dp +private val CIRCLE_GAP: Dp = CIRCLE_PADDING_START - CIRCLE_SIZE + +@Composable +fun CommentTextField( + uiModel: CommentUiModel, + onCommentChanged: (TextFieldValue) -> Unit, + onFocusChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + val interactionSource = remember { MutableInteractionSource() } + val placeholder = stringResource(R.string.comment_text_field_placeholder) + + Box( + modifier = + modifier + .clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusRequester.requestFocus() + }, + ) { + BasicTextField( + value = uiModel.comment, + onValueChange = { newValue -> onCommentChanged(newValue) }, + modifier = + Modifier + .alpha(0f) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + onFocusChanged(focusState.isFocused) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + singleLine = true, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(CIRCLE_GAP), + modifier = + Modifier.drawWithCache { + val radius = size.height / 2 + val paddingStart = CIRCLE_PADDING_START.toPx() + + onDrawBehind { + repeat(CommentUiModel.COMMENT_COUNT) { index -> + val cx = radius + index * paddingStart + + drawCircle( + color = Color.Black, + radius = radius, + center = Offset(cx, radius), + style = Stroke(2.dp.toPx()), + ) + } + } + }, + ) { + repeat(CommentUiModel.COMMENT_COUNT) { index -> + val char = + if (uiModel.hidePlaceholder) { + uiModel.comment.text + .getOrNull(index) + ?.toString() + } else { + placeholder.getOrNull(index)?.toString() + }.orEmpty() + + CommentCircle( + text = char, + showPlaceholder = !uiModel.hidePlaceholder, + showCursor = uiModel.showCursor(index), + modifier = + Modifier.clickable( + indication = null, + interactionSource = interactionSource, + ) { + focusRequester.requestFocus() + onCommentChanged(uiModel.comment.copy(selection = TextRange(uiModel.comment.text.length))) + }, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CommentTextFieldPreview() { + TwixTheme { + var text by remember { mutableStateOf(TextFieldValue("")) } + var isFocused by remember { mutableStateOf(false) } + CommentTextField( + uiModel = CommentUiModel(text, isFocused), + onCommentChanged = { text = it }, + onFocusChanged = { isFocused = it }, + ) + } +} 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 new file mode 100644 index 0000000..ba2e3d0 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt @@ -0,0 +1,44 @@ +package com.twix.designsystem.components.comment.model + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue + +@Immutable +data class CommentUiModel( + val comment: TextFieldValue = TextFieldValue(""), + val isFocused: Boolean = false, +) { + fun updateComment(comment: TextFieldValue) = copy(comment = comment.copy(comment.text.take(COMMENT_COUNT))) + + fun updateFocus(isFocused: Boolean) = copy(isFocused = isFocused) + + /** + * 특정 index 위치에 커서를 표시할지 여부를 반환한다. + * + * 표시 조건 + * 1. 포커스 상태일 것 + * 2. 현재 selection 시작 위치가 해당 index 일 것 + * 3. 해당 위치에 문자가 없을 것 (빈 칸) + * + * @param index 확인할 문자 위치 + */ + fun showCursor(index: Int): Boolean { + val isCharEmpty = comment.text.getOrNull(index) == null + return isFocused && comment.selection.start == index && isCharEmpty + } + + /** + * 플레이스홀더 표시 여부. + * + * 표시 조건 + * - 포커스 중이 아니거나 + * - 텍스트가 하나라도 존재하지 않으면 + * + */ + val hidePlaceholder: Boolean + get() = isFocused || comment.text.isNotEmpty() + + companion object { + const val COMMENT_COUNT = 5 + } +} diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index da500dd..7a32a28 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -17,5 +17,7 @@ 오늘 우리 목표 첫 목표를 세워볼까요? - - \ No newline at end of file + + + 코멘트추가 + From 0f13b3c68d7377e5ca128da190c9ffbacd437794 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 13:07:53 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=83=B7=20=ED=99=94=EB=A9=B4=EC=97=90=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TaskCertificationScreen.kt | 32 ++++++++++++++++--- .../TaskCertificationViewModel.kt | 16 ++++++++++ .../component/CameraPreviewBox.kt | 26 ++++++++++++--- .../model/TaskCertificationIntent.kt | 9 ++++++ .../model/TaskCertificationUiState.kt | 11 +++++++ 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt index 52d194d..55c5d32 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt @@ -10,6 +10,7 @@ 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.foundation.layout.imePadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -18,7 +19,11 @@ import androidx.compose.runtime.remember 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.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -38,6 +43,7 @@ import com.twix.task_certification.model.TaskCertificationIntent import com.twix.task_certification.model.TaskCertificationSideEffect import com.twix.task_certification.model.TaskCertificationUiState import com.twix.ui.base.ObserveAsEvents +import com.twix.ui.extension.noRippleClickable import com.twix.ui.toast.ToastManager import com.twix.ui.toast.model.ToastData import com.twix.ui.toast.model.ToastType @@ -57,7 +63,7 @@ fun TaskCertificationRoute( val lifecycleOwner = LocalLifecycleOwner.current val coroutineScope = lifecycleOwner.lifecycle.coroutineScope - val context = androidx.compose.ui.platform.LocalContext.current + val context = LocalContext.current var hasPermission by remember { mutableStateOf( @@ -140,6 +146,12 @@ fun TaskCertificationRoute( onClickRefresh = { viewModel.dispatch(TaskCertificationIntent.RetakePicture) }, + onCommentChanged = { + viewModel.dispatch(TaskCertificationIntent.UpdateComment(it)) + }, + onFocusChanged = { + viewModel.dispatch(TaskCertificationIntent.CommentFocusChanged(it)) + }, onClickUpload = { }, ) } @@ -155,11 +167,18 @@ private fun TaskCertificationScreen( onClickGallery: () -> Unit, onClickRefresh: () -> Unit, onClickUpload: () -> Unit, + onCommentChanged: (TextFieldValue) -> Unit, + onFocusChanged: (Boolean) -> Unit, ) { + val keyboardController = LocalSoftwareKeyboardController.current + val fucusManager = LocalFocusManager.current + Column( Modifier .fillMaxSize() - .background(GrayColor.C500), + .background(GrayColor.C500) + .noRippleClickable { fucusManager.clearFocus() } + .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { TaskCertificationTopBar( @@ -178,13 +197,14 @@ private fun TaskCertificationScreen( CameraPreviewBox( capture = uiState.capture, + commentUiModel = uiState.commentUiModel, previewRequest = cameraPreview, torch = uiState.torch, - onClickFlash = { onClickFlash() }, + onClickFlash = onClickFlash, + onCommentChanged = onCommentChanged, + onFocusChanged = onFocusChanged, ) - Spacer(modifier = Modifier.height(52.dp)) - CameraControlBar( capture = uiState.capture, onCaptureClick = onCaptureClick, @@ -210,6 +230,8 @@ fun TaskCertificationScreenPreview() { onClickGallery = {}, onClickRefresh = {}, onClickUpload = {}, + onCommentChanged = {}, + onFocusChanged = {}, ) } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index 6114cf1..d9fcde1 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -34,6 +34,14 @@ class TaskCertificationViewModel : is TaskCertificationIntent.RetakePicture -> { setupRetake() } + + is TaskCertificationIntent.UpdateComment -> { + updateComment(intent) + } + + is TaskCertificationIntent.CommentFocusChanged -> { + updateCommentFocus(intent.isFocused) + } } } @@ -67,4 +75,12 @@ class TaskCertificationViewModel : private fun setupRetake() { reduce { removePicture() } } + + private fun updateComment(intent: TaskCertificationIntent.UpdateComment) { + reduce { updateComment(intent.comment) } + } + + private fun updateCommentFocus(isFocused: Boolean) { + reduce { updateCommentFocus(isFocused) } + } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt index 677ff07..ee26a53 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt @@ -4,20 +4,23 @@ import androidx.camera.compose.CameraXViewfinder import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.twix.designsystem.components.comment.CommentTextField +import com.twix.designsystem.components.comment.model.CommentUiModel import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.task_certification.R @@ -28,18 +31,20 @@ import com.twix.ui.extension.noRippleClickable @Composable fun CameraPreviewBox( + commentUiModel: CommentUiModel, capture: CaptureStatus, previewRequest: CameraPreview?, torch: TorchStatus, onClickFlash: () -> Unit, + onCommentChanged: (TextFieldValue) -> Unit, + onFocusChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { Box( modifier = modifier - .fillMaxWidth() + .size(375.66.dp) .padding(horizontal = 5.dp) - .aspectRatio(1f) .border( color = GrayColor.C400, width = 2.dp, @@ -51,6 +56,16 @@ fun CameraPreviewBox( if (capture == CaptureStatus.NotCaptured) { TorchIcon(torch, onClickFlash) } + + CommentTextField( + uiModel = commentUiModel, + onCommentChanged = onCommentChanged, + onFocusChanged = onFocusChanged, + modifier = + Modifier + .padding(bottom = 20.dp) + .align(Alignment.BottomCenter), + ) } } @@ -106,10 +121,13 @@ private fun TorchIcon( fun CameraPreviewBoxNotCapturedPreview() { TwixTheme { CameraPreviewBox( + commentUiModel = CommentUiModel(), capture = CaptureStatus.NotCaptured, torch = TorchStatus.Off, previewRequest = null, onClickFlash = {}, + onCommentChanged = {}, + onFocusChanged = {}, ) } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt index 6e34a23..b5ad1c2 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt @@ -1,6 +1,7 @@ package com.twix.task_certification.model import android.net.Uri +import androidx.compose.ui.text.input.TextFieldValue import com.twix.ui.base.Intent sealed interface TaskCertificationIntent : Intent { @@ -17,4 +18,12 @@ sealed interface TaskCertificationIntent : Intent { data object ToggleFlash : TaskCertificationIntent data object RetakePicture : TaskCertificationIntent + + data class UpdateComment( + val comment: TextFieldValue, + ) : TaskCertificationIntent + + data class CommentFocusChanged( + val isFocused: Boolean, + ) : TaskCertificationIntent } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt index 4cc38a4..3f90d87 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt @@ -3,6 +3,8 @@ package com.twix.task_certification.model import android.net.Uri import androidx.camera.core.CameraSelector import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue +import com.twix.designsystem.components.comment.model.CommentUiModel import com.twix.ui.base.State @Immutable @@ -11,6 +13,7 @@ data class TaskCertificationUiState( val torch: TorchStatus = TorchStatus.Off, val lens: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, val preview: CameraPreview? = null, + val commentUiModel: CommentUiModel = CommentUiModel(), ) : State { fun toggleLens(): TaskCertificationUiState { val newLens = @@ -30,4 +33,12 @@ data class TaskCertificationUiState( fun updateCapturedImage(uri: Uri) = copy(capture = CaptureStatus.Captured(uri)) fun removePicture(): TaskCertificationUiState = copy(capture = CaptureStatus.NotCaptured) + + fun updateComment(comment: TextFieldValue) = copy(commentUiModel = commentUiModel.updateComment(comment)) + + fun updateCommentFocus(isFocused: Boolean) = copy(commentUiModel = commentUiModel.updateFocus(isFocused)) + + companion object { + private const val MAX_COMMENT_LENGTH = 5 + } } From 361f852c6101574d82822aafaddd229f3834a2ef Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 13:20:04 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/CameraPreviewBox.kt | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt index ee26a53..0f2888a 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt @@ -4,7 +4,10 @@ import androidx.camera.compose.CameraXViewfinder import androidx.compose.foundation.Image import androidx.compose.foundation.border 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.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -21,8 +24,10 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.twix.designsystem.components.comment.CommentTextField import com.twix.designsystem.components.comment.model.CommentUiModel +import com.twix.designsystem.components.text.AppText 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.model.CameraPreview import com.twix.task_certification.model.CaptureStatus @@ -49,7 +54,8 @@ fun CameraPreviewBox( color = GrayColor.C400, width = 2.dp, shape = RoundedCornerShape(73.83.dp), - ).clip(RoundedCornerShape(73.83.dp)), + ) + .clip(RoundedCornerShape(73.83.dp)), ) { CameraSurface(capture, previewRequest) @@ -57,15 +63,28 @@ fun CameraPreviewBox( TorchIcon(torch, onClickFlash) } - CommentTextField( - uiModel = commentUiModel, - onCommentChanged = onCommentChanged, - onFocusChanged = onFocusChanged, - modifier = - Modifier - .padding(bottom = 20.dp) - .align(Alignment.BottomCenter), - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.align(Alignment.BottomCenter), + ) { + if (commentUiModel.isFocused) { + AppText( + text = "5글자로 코멘트를 남길 수 있어요", + style = AppTextStyle.B2, + color = GrayColor.C100, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + CommentTextField( + uiModel = commentUiModel, + onCommentChanged = onCommentChanged, + onFocusChanged = onFocusChanged, + modifier = + Modifier + .padding(bottom = 20.dp), + ) + } } } @@ -121,7 +140,7 @@ private fun TorchIcon( fun CameraPreviewBoxNotCapturedPreview() { TwixTheme { CameraPreviewBox( - commentUiModel = CommentUiModel(), + commentUiModel = CommentUiModel(isFocused = true), capture = CaptureStatus.NotCaptured, torch = TorchStatus.Off, previewRequest = null, From 15383db7b168b5e3740173bada9ee5dce7703998 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 15:51:14 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=97=B4=EB=A6=BC/=EB=8B=AB=ED=9E=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EA=B4=80=EC=B0=B0=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/keyboard/KeyBoardAsState.kt | 33 +++++++++++++++++++ .../twix/designsystem/keyboard/Keyboard.kt | 6 ++++ 2 files changed, 39 insertions(+) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/keyboard/KeyBoardAsState.kt create mode 100644 core/design-system/src/main/java/com/twix/designsystem/keyboard/Keyboard.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/keyboard/KeyBoardAsState.kt b/core/design-system/src/main/java/com/twix/designsystem/keyboard/KeyBoardAsState.kt new file mode 100644 index 0000000..56a54a9 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/keyboard/KeyBoardAsState.kt @@ -0,0 +1,33 @@ +package com.twix.designsystem.keyboard + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalDensity + +// TODO : core:ui 모듈로 이동 + +/** + * 현재 소프트 키보드(IME)의 열림/닫힘 상태를 [Keyboard] 값으로 제공하는 Composable 유틸 함수. + * + * [WindowInsets.ime] 의 bottom inset 값을 사용하여 + * 키보드가 화면을 밀어 올리고 있는지 여부를 감지합니다. + * + * - inset > 0 → [Keyboard.Opened] + * - inset == 0 → [Keyboard.Closed] + * + * @return 현재 키보드 상태를 나타내는 [State]<[Keyboard]> + */ +@Composable +fun keyboardAsState(): State { + val density = LocalDensity.current + val keyboard = + if (WindowInsets.ime.getBottom(density) > 0) { + Keyboard.Opened + } else { + Keyboard.Closed + } + return rememberUpdatedState(keyboard) +} diff --git a/core/design-system/src/main/java/com/twix/designsystem/keyboard/Keyboard.kt b/core/design-system/src/main/java/com/twix/designsystem/keyboard/Keyboard.kt new file mode 100644 index 0000000..414a38e --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/keyboard/Keyboard.kt @@ -0,0 +1,6 @@ +package com.twix.designsystem.keyboard + +enum class Keyboard { + Opened, + Closed, +} From 4f84457d6f39087db75b604c00eb4d1a9303aa8d Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 15:52:31 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20viewModel=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/task_certification/TaskCertificationViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index d9fcde1..632f150 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -46,7 +46,7 @@ class TaskCertificationViewModel : } private fun takePicture(uri: Uri?) { - uri?.let { updatePickPicture(it) } ?: viewModelScope.launch { + uri?.let { updatePicture(it) } ?: viewModelScope.launch { emitSideEffect( TaskCertificationSideEffect.ImageCaptureFailException, ) @@ -54,10 +54,10 @@ class TaskCertificationViewModel : } private fun pickPicture(uri: Uri?) { - uri?.let { updatePickPicture(uri) } + uri?.let { updatePicture(uri) } } - private fun updatePickPicture(uri: Uri) { + private fun updatePicture(uri: Uri) { reduce { updateCapturedImage(uri) } if (uiState.value.torch == TorchStatus.On) { reduce { toggleTorch() } From d4b1a2ff4b8caa409e03baa8ae65c374591a10a7 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 15:56:58 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=EC=8B=9C=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=85=EB=A0=A5=EC=B0=BD=EC=97=90=20=ED=8F=AC?= =?UTF-8?q?=EC=BB=A4=EC=8B=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/comment/CommentTextField.kt | 38 +++++++++++++++++-- .../comment/model/CommentUiModel.kt | 3 ++ .../TaskCertificationScreen.kt | 2 - .../TaskCertificationViewModel.kt | 4 ++ .../model/TaskCertificationUiState.kt | 7 ++-- 5 files changed, 44 insertions(+), 10 deletions(-) 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 9afed87..9868b9f 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 @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -21,6 +22,8 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction @@ -30,7 +33,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.twix.designsystem.R import com.twix.designsystem.components.comment.model.CommentUiModel +import com.twix.designsystem.keyboard.Keyboard +import com.twix.designsystem.keyboard.keyboardAsState import com.twix.designsystem.theme.TwixTheme +import kotlinx.coroutines.android.awaitFrame private val CIRCLE_PADDING_START: Dp = 50.dp private val CIRCLE_SIZE: Dp = 64.dp @@ -43,19 +49,43 @@ fun CommentTextField( onFocusChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { + val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current val interactionSource = remember { MutableInteractionSource() } val placeholder = stringResource(R.string.comment_text_field_placeholder) + val keyboardVisibility by keyboardAsState() + + LaunchedEffect(keyboardVisibility) { + when (keyboardVisibility) { + Keyboard.Opened -> Unit + Keyboard.Closed -> { + focusManager.clearFocus() + onFocusChanged(false) + } + } + } + + LaunchedEffect(uiModel.isFocused) { + if (uiModel.isFocused) { + focusRequester.requestFocus() + awaitFrame() + keyboardController?.show() + } else { + keyboardController?.hide() + } + } + Box( modifier = modifier .clickable( - indication = null, + // TODO : noClickableRipple 로 수정 interactionSource = interactionSource, - ) { - focusRequester.requestFocus() - }, + indication = null, + onClick = { focusRequester.requestFocus() }, + ), ) { BasicTextField( value = uiModel.comment, 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 ba2e3d0..08403cc 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 @@ -8,6 +8,9 @@ data class CommentUiModel( val comment: TextFieldValue = TextFieldValue(""), val isFocused: Boolean = false, ) { + val hasMaxCommentLength: Boolean + get() = comment.text.length == COMMENT_COUNT + fun updateComment(comment: TextFieldValue) = copy(comment = comment.copy(comment.text.take(COMMENT_COUNT))) fun updateFocus(isFocused: Boolean) = copy(isFocused = isFocused) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt index 55c5d32..12981cb 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview @@ -170,7 +169,6 @@ private fun TaskCertificationScreen( onCommentChanged: (TextFieldValue) -> Unit, onFocusChanged: (Boolean) -> Unit, ) { - val keyboardController = LocalSoftwareKeyboardController.current val fucusManager = LocalFocusManager.current Column( diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index 632f150..9dfe816 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -62,6 +62,10 @@ class TaskCertificationViewModel : if (uiState.value.torch == TorchStatus.On) { reduce { toggleTorch() } } + + if (uiState.value.hasMaxCommentLength.not()) { + updateCommentFocus(true) + } } private fun toggleLens() { diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt index 3f90d87..e89ff99 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt @@ -15,6 +15,9 @@ data class TaskCertificationUiState( val preview: CameraPreview? = null, val commentUiModel: CommentUiModel = CommentUiModel(), ) : State { + val hasMaxCommentLength: Boolean + get() = commentUiModel.hasMaxCommentLength + fun toggleLens(): TaskCertificationUiState { val newLens = if (lens == CameraSelector.DEFAULT_BACK_CAMERA) { @@ -37,8 +40,4 @@ data class TaskCertificationUiState( fun updateComment(comment: TextFieldValue) = copy(commentUiModel = commentUiModel.updateComment(comment)) fun updateCommentFocus(isFocused: Boolean) = copy(commentUiModel = commentUiModel.updateFocus(isFocused)) - - companion object { - private const val MAX_COMMENT_LENGTH = 5 - } } From ed3a8c422b3b23207f92585e74028fbdbc7fd51a Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 16:11:21 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=BD=94=EB=A9=98=ED=8A=B8=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EC=A0=80?= =?UTF-8?q?=EB=B8=94=EC=9D=84=20commentTextfield=EC=99=80=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/comment/CommentBox.kt | 48 ++++++++++++++++++ .../components/comment/CommentTextField.kt | 4 +- .../src/main/res/values/strings.xml | 1 + .../component/CameraPreviewBox.kt | 38 +++----------- .../src/main/res/drawable/btn.png | Bin 6755 -> 0 bytes 5 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt delete mode 100644 feature/task-certification/src/main/res/drawable/btn.png 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 new file mode 100644 index 0000000..1582ee9 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/comment/CommentBox.kt @@ -0,0 +1,48 @@ +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.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.comment.model.CommentUiModel +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle + +@Composable +fun CommentBox( + uiModel: CommentUiModel, + onCommentChanged: (TextFieldValue) -> Unit, + onFocusChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + if (uiModel.isFocused) { + AppText( + text = stringResource(R.string.comment_condition_guide), + style = AppTextStyle.B2, + color = GrayColor.C100, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + CommentTextField( + uiModel = uiModel, + onCommentChanged = 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 9868b9f..12e27e9 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 @@ -20,7 +20,6 @@ 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.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -35,6 +34,7 @@ import com.twix.designsystem.R import com.twix.designsystem.components.comment.model.CommentUiModel import com.twix.designsystem.keyboard.Keyboard import com.twix.designsystem.keyboard.keyboardAsState +import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import kotlinx.coroutines.android.awaitFrame @@ -113,7 +113,7 @@ fun CommentTextField( val cx = radius + index * paddingStart drawCircle( - color = Color.Black, + color = GrayColor.C500, radius = radius, center = Offset(cx, radius), style = Stroke(2.dp.toPx()), diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 7a32a28..b06dec3 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ 코멘트추가 + 5글자로 코멘트를 남길 수 있어요 diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt index 0f2888a..02f9c5b 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt @@ -4,10 +4,7 @@ import androidx.camera.compose.CameraXViewfinder import androidx.compose.foundation.Image import androidx.compose.foundation.border 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.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -22,12 +19,10 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.twix.designsystem.components.comment.CommentTextField +import com.twix.designsystem.components.comment.CommentBox import com.twix.designsystem.components.comment.model.CommentUiModel -import com.twix.designsystem.components.text.AppText 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.model.CameraPreview import com.twix.task_certification.model.CaptureStatus @@ -54,8 +49,7 @@ fun CameraPreviewBox( color = GrayColor.C400, width = 2.dp, shape = RoundedCornerShape(73.83.dp), - ) - .clip(RoundedCornerShape(73.83.dp)), + ).clip(RoundedCornerShape(73.83.dp)), ) { CameraSurface(capture, previewRequest) @@ -63,28 +57,12 @@ fun CameraPreviewBox( TorchIcon(torch, onClickFlash) } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.align(Alignment.BottomCenter), - ) { - if (commentUiModel.isFocused) { - AppText( - text = "5글자로 코멘트를 남길 수 있어요", - style = AppTextStyle.B2, - color = GrayColor.C100, - ) - - Spacer(modifier = Modifier.height(8.dp)) - } - CommentTextField( - uiModel = commentUiModel, - onCommentChanged = onCommentChanged, - onFocusChanged = onFocusChanged, - modifier = - Modifier - .padding(bottom = 20.dp), - ) - } + CommentBox( + uiModel = commentUiModel, + onCommentChanged = onCommentChanged, + onFocusChanged = onFocusChanged, + modifier = Modifier.align(Alignment.BottomCenter), + ) } } diff --git a/feature/task-certification/src/main/res/drawable/btn.png b/feature/task-certification/src/main/res/drawable/btn.png deleted file mode 100644 index 0b1fe8332fe6782d7d3220484298aa18946442ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6755 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 FV1nn9+av%0 From b4f79d31e5a988ba5fa3f3f7c08a5a7c866fc45f Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sat, 31 Jan 2026 17:13:27 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=20=EC=9C=A0=ED=9A=A8=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9D=84=EC=8B=9C=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/model/CommentUiModel.kt | 9 ++++ .../TaskCertificationScreen.kt | 22 ++++++--- .../TaskCertificationViewModel.kt | 17 +++++++ .../component/CommentErrorText.kt | 47 +++++++++++++++++++ .../model/TaskCertificationIntent.kt | 2 + .../model/TaskCertificationUiState.kt | 5 ++ .../src/main/res/values/strings.xml | 2 +- 7 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 feature/task-certification/src/main/java/com/twix/task_certification/component/CommentErrorText.kt 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 08403cc..1fa0ad5 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 @@ -8,9 +8,18 @@ data class CommentUiModel( val comment: TextFieldValue = TextFieldValue(""), val isFocused: Boolean = false, ) { + val isEmpty: Boolean + get() = comment.text.isEmpty() + val hasMaxCommentLength: Boolean get() = comment.text.length == COMMENT_COUNT + val canUpload: Boolean + get() = + comment.text.isEmpty() || + comment.text.isNotEmpty() && + hasMaxCommentLength + fun updateComment(comment: TextFieldValue) = copy(comment = comment.copy(comment.text.take(COMMENT_COUNT))) fun updateFocus(isFocused: Boolean) = copy(isFocused = isFocused) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt index 12981cb..554bef3 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -36,6 +37,7 @@ import com.twix.domain.model.enums.AppTextStyle import com.twix.task_certification.camera.Camera import com.twix.task_certification.component.CameraControlBar import com.twix.task_certification.component.CameraPreviewBox +import com.twix.task_certification.component.CommentErrorText import com.twix.task_certification.component.TaskCertificationTopBar import com.twix.task_certification.model.CameraPreview import com.twix.task_certification.model.TaskCertificationIntent @@ -151,7 +153,9 @@ fun TaskCertificationRoute( onFocusChanged = { viewModel.dispatch(TaskCertificationIntent.CommentFocusChanged(it)) }, - onClickUpload = { }, + onClickUpload = { + viewModel.dispatch(TaskCertificationIntent.Upload) + }, ) } @@ -185,11 +189,17 @@ private fun TaskCertificationScreen( Spacer(modifier = Modifier.height(24.26.dp)) - AppText( - text = stringResource(R.string.task_certification_title), - style = AppTextStyle.H2, - color = GrayColor.C100, - ) + AnimatedContent(targetState = uiState.showCommentError) { isError -> + if (isError) { + CommentErrorText() + } else { + AppText( + text = stringResource(R.string.task_certification_title), + style = AppTextStyle.H2, + color = GrayColor.C100, + ) + } + } Spacer(modifier = Modifier.height(40.dp)) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index 9dfe816..4ed0de3 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -7,6 +7,7 @@ import com.twix.task_certification.model.TaskCertificationSideEffect import com.twix.task_certification.model.TaskCertificationUiState import com.twix.task_certification.model.TorchStatus import com.twix.ui.base.BaseViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch class TaskCertificationViewModel : @@ -42,6 +43,10 @@ class TaskCertificationViewModel : is TaskCertificationIntent.CommentFocusChanged -> { updateCommentFocus(intent.isFocused) } + + is TaskCertificationIntent.Upload -> { + upload() + } } } @@ -87,4 +92,16 @@ class TaskCertificationViewModel : private fun updateCommentFocus(isFocused: Boolean) { reduce { updateCommentFocus(isFocused) } } + + private fun upload() { + if (uiState.value.commentUiModel.canUpload + .not() + ) { + viewModelScope.launch { + reduce { showCommentError() } + delay(1500) + reduce { hideCommentError() } + } + } + } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CommentErrorText.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CommentErrorText.kt new file mode 100644 index 0000000..d0a2b49 --- /dev/null +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CommentErrorText.kt @@ -0,0 +1,47 @@ +package com.twix.task_certification.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.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) { + Box( + modifier = + modifier + .width(224.dp) + .height(56.dp) + .background(color = GrayColor.C400, shape = RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(12.dp)), + ) { + AppText( + text = stringResource(R.string.comment_error_message), + style = AppTextStyle.B1, + color = CommonColor.White, + modifier = Modifier.align(Alignment.Center), + ) + } +} + +@Preview +@Composable +fun CommentErrorTextPreview() { + TwixTheme { + CommentErrorText() + } +} diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt index b5ad1c2..c8da39e 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationIntent.kt @@ -26,4 +26,6 @@ sealed interface TaskCertificationIntent : Intent { data class CommentFocusChanged( val isFocused: Boolean, ) : TaskCertificationIntent + + data object Upload : TaskCertificationIntent } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt index e89ff99..91da5c6 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt @@ -14,6 +14,7 @@ data class TaskCertificationUiState( val lens: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, val preview: CameraPreview? = null, val commentUiModel: CommentUiModel = CommentUiModel(), + val showCommentError: Boolean = false, ) : State { val hasMaxCommentLength: Boolean get() = commentUiModel.hasMaxCommentLength @@ -40,4 +41,8 @@ data class TaskCertificationUiState( fun updateComment(comment: TextFieldValue) = copy(commentUiModel = commentUiModel.updateComment(comment)) fun updateCommentFocus(isFocused: Boolean) = copy(commentUiModel = commentUiModel.updateFocus(isFocused)) + + fun showCommentError() = copy(showCommentError = true) + + fun hideCommentError() = copy(showCommentError = false) } diff --git a/feature/task-certification/src/main/res/values/strings.xml b/feature/task-certification/src/main/res/values/strings.xml index 7e10708..0ff1257 100644 --- a/feature/task-certification/src/main/res/values/strings.xml +++ b/feature/task-certification/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ 인증샷 찍기 이미지 캡처에 실패했습니다. 다시 시도해 주세요. - 이미지를 불러오는 데 실패했습니다. 다시 시도해 주세요. + 코멘트는 5글자로 입력해주세요! From 7fc00bb8f5ac04832d7b73a1b567ec27f2106d59 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 1 Feb 2026 11:59:19 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=8B=9C=20Dimmed=20=ED=9A=A8=EA=B3=BC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/comment/CommentBox.kt | 10 +++ .../components/comment/CommentTextField.kt | 13 ++- .../TaskCertificationScreen.kt | 84 ++++++++++++++++++- .../component/CameraPreviewBox.kt | 9 +- 4 files changed, 110 insertions(+), 6 deletions(-) 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 1582ee9..f15d9d2 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 @@ -7,6 +7,9 @@ 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.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp @@ -21,6 +24,8 @@ fun CommentBox( uiModel: CommentUiModel, onCommentChanged: (TextFieldValue) -> Unit, onFocusChanged: (Boolean) -> Unit, + onGuideTextPositioned: (Rect) -> Unit, + onTextFieldPositioned: (Rect) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -32,6 +37,10 @@ fun CommentBox( text = stringResource(R.string.comment_condition_guide), style = AppTextStyle.B2, color = GrayColor.C100, + modifier = + Modifier.onGloballyPositioned { + onGuideTextPositioned(it.boundsInRoot()) + }, ) Spacer(modifier = Modifier.height(8.dp)) @@ -40,6 +49,7 @@ fun CommentBox( uiModel = uiModel, onCommentChanged = onCommentChanged, onFocusChanged = onFocusChanged, + onPositioned = onTextFieldPositioned, 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 12e27e9..44d3a24 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 @@ -20,7 +20,10 @@ 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.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -38,8 +41,8 @@ import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import kotlinx.coroutines.android.awaitFrame -private val CIRCLE_PADDING_START: Dp = 50.dp -private val CIRCLE_SIZE: Dp = 64.dp +val CIRCLE_PADDING_START: Dp = 50.dp +val CIRCLE_SIZE: Dp = 64.dp private val CIRCLE_GAP: Dp = CIRCLE_PADDING_START - CIRCLE_SIZE @Composable @@ -47,6 +50,7 @@ fun CommentTextField( uiModel: CommentUiModel, onCommentChanged: (TextFieldValue) -> Unit, onFocusChanged: (Boolean) -> Unit, + onPositioned: (Rect) -> Unit, modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current @@ -80,7 +84,9 @@ fun CommentTextField( Box( modifier = modifier - .clickable( + .onGloballyPositioned { coordinates -> + onPositioned(coordinates.boundsInRoot()) + }.clickable( // TODO : noClickableRipple 로 수정 interactionSource = interactionSource, indication = null, @@ -160,6 +166,7 @@ private fun CommentTextFieldPreview() { uiModel = CommentUiModel(text, isFocused), onCommentChanged = { text = it }, onFocusChanged = { isFocused = it }, + onPositioned = {}, ) } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt index 554bef3..455a72c 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt @@ -6,12 +6,15 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Canvas 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.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -20,17 +23,30 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer 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.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.coroutineScope +import com.twix.designsystem.components.comment.CIRCLE_PADDING_START +import com.twix.designsystem.components.comment.CIRCLE_SIZE +import com.twix.designsystem.components.comment.model.CommentUiModel import com.twix.designsystem.components.text.AppText +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 @@ -51,6 +67,7 @@ import com.twix.ui.toast.model.ToastType import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject +import com.twix.designsystem.R as DesR @Composable fun TaskCertificationRoute( @@ -173,13 +190,15 @@ private fun TaskCertificationScreen( onCommentChanged: (TextFieldValue) -> Unit, onFocusChanged: (Boolean) -> Unit, ) { - val fucusManager = LocalFocusManager.current + val focusManager = LocalFocusManager.current + var textFieldRect by remember { mutableStateOf(Rect.Zero) } + var guideTextRect by remember { mutableStateOf(Rect.Zero) } Column( Modifier .fillMaxSize() .background(GrayColor.C500) - .noRippleClickable { fucusManager.clearFocus() } + .noRippleClickable { focusManager.clearFocus() } .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -211,6 +230,8 @@ private fun TaskCertificationScreen( onClickFlash = onClickFlash, onCommentChanged = onCommentChanged, onFocusChanged = onFocusChanged, + onGuideTextPositioned = { guideTextRect = it }, + onTextFieldPositioned = { textFieldRect = it }, ) CameraControlBar( @@ -222,6 +243,65 @@ private fun TaskCertificationScreen( onClickUpload = onClickUpload, ) } + + DimmedScreen( + isFocused = uiState.commentUiModel.isFocused, + textFieldRect = textFieldRect, + guideTextRect = guideTextRect, + focusManager = focusManager, + ) +} + +@Composable +fun DimmedScreen( + isFocused: Boolean, + textFieldRect: Rect, + guideTextRect: Rect, + focusManager: FocusManager, +) { + if (isFocused && textFieldRect != Rect.Zero) { + val density = LocalDensity.current + val circleSizePx = with(density) { CIRCLE_SIZE.toPx() } + val radiusPx = circleSizePx / 2 + val paddingStartPx = with(density) { CIRCLE_PADDING_START.toPx() } + + Canvas( + modifier = + Modifier + .fillMaxSize() + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .noRippleClickable { focusManager.clearFocus() }, + ) { + drawRect(color = DimmedColor.D070) + + repeat(CommentUiModel.COMMENT_COUNT) { index -> + val cx = textFieldRect.left + radiusPx + (index * paddingStartPx) + val cy = textFieldRect.top + radiusPx + + drawCircle( + color = Color.Transparent, + radius = radiusPx, + center = Offset(cx, cy), + blendMode = BlendMode.Clear, + ) + } + } + + if (guideTextRect != Rect.Zero) { + Box( + modifier = + Modifier.offset { + IntOffset(guideTextRect.left.toInt(), guideTextRect.top.toInt()) + }, + ) { + AppText( + text = stringResource(DesR.string.comment_condition_guide), + style = AppTextStyle.B2, + color = GrayColor.C100, + ) + } + } + } } @Preview diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt index 02f9c5b..474b006 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource @@ -38,6 +39,8 @@ fun CameraPreviewBox( onClickFlash: () -> Unit, onCommentChanged: (TextFieldValue) -> Unit, onFocusChanged: (Boolean) -> Unit, + onGuideTextPositioned: (Rect) -> Unit, + onTextFieldPositioned: (Rect) -> Unit, modifier: Modifier = Modifier, ) { Box( @@ -53,7 +56,7 @@ fun CameraPreviewBox( ) { CameraSurface(capture, previewRequest) - if (capture == CaptureStatus.NotCaptured) { + if (capture is CaptureStatus.NotCaptured) { TorchIcon(torch, onClickFlash) } @@ -61,6 +64,8 @@ fun CameraPreviewBox( uiModel = commentUiModel, onCommentChanged = onCommentChanged, onFocusChanged = onFocusChanged, + onGuideTextPositioned = onGuideTextPositioned, + onTextFieldPositioned = onTextFieldPositioned, modifier = Modifier.align(Alignment.BottomCenter), ) } @@ -125,6 +130,8 @@ fun CameraPreviewBoxNotCapturedPreview() { onClickFlash = {}, onCommentChanged = {}, onFocusChanged = {}, + onGuideTextPositioned = {}, + onTextFieldPositioned = {}, ) } } From 33bba6c643df18eae5a1f29f12f6c9f37dd001ef Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 1 Feb 2026 13:24:43 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=E2=9C=A8=20Feat:=20enableEdgeToEdge=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/yapp/twix/main/MainActivity.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/yapp/twix/main/MainActivity.kt b/app/src/main/java/com/yapp/twix/main/MainActivity.kt index 4de9cb2..733ea86 100644 --- a/app/src/main/java/com/yapp/twix/main/MainActivity.kt +++ b/app/src/main/java/com/yapp/twix/main/MainActivity.kt @@ -3,11 +3,13 @@ package com.yapp.twix.main import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat import com.twix.designsystem.theme.TwixTheme import com.twix.navigation.AppNavHost import com.twix.ui.toast.ToastHost @@ -17,15 +19,16 @@ import org.koin.android.ext.android.inject class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + enableEdgeToEdge() setContent { val toastManager by inject() + WindowCompat.setDecorFitsSystemWindows(window, false) + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true TwixTheme { Box( modifier = Modifier - .safeContentPadding() .fillMaxSize(), ) { AppNavHost() From b1afa3eb32283548621122c24f99aa5cc990e2c1 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 1 Feb 2026 13:37:43 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20Spacer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/task_certification/TaskCertificationScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt index 455a72c..c229d4b 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationScreen.kt @@ -234,6 +234,8 @@ private fun TaskCertificationScreen( onTextFieldPositioned = { textFieldRect = it }, ) + Spacer(modifier = Modifier.height(52.dp)) + CameraControlBar( capture = uiState.capture, onCaptureClick = onCaptureClick, From 097d920ef44227c1829b15b8e76486831d84da31 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Sun, 1 Feb 2026 13:42:54 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=BD=94=EB=A9=98=ED=8A=B8=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=9C=EB=A0=A5=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/task_certification/TaskCertificationViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index 4ed0de3..38e2214 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -99,9 +99,13 @@ class TaskCertificationViewModel : ) { viewModelScope.launch { reduce { showCommentError() } - delay(1500) + delay(ERROR_DISPLAY_DURATION_MS) reduce { hideCommentError() } } } } + + companion object { + private const val ERROR_DISPLAY_DURATION_MS = 1500L + } } From 1ca578517f044b0827515f6712d59190964f7fc5 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 4 Feb 2026 10:26:57 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EA=B5=AC=EB=B2=84=EC=A0=84=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/task_certification/TaskCertificationViewModel.kt | 2 +- .../twix/task_certification/model/TaskCertificationUiState.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt index df88aa6..d62830b 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/TaskCertificationViewModel.kt @@ -63,7 +63,7 @@ class TaskCertificationViewModel : } private fun reducePicture(uri: Uri) { - reduce { updateCapturedImage(uri) } + reduce { updatePicture(uri) } if (uiState.value.torch == TorchStatus.On) { reduce { toggleTorch() } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt index 89f01b2..3b1d278 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/model/TaskCertificationUiState.kt @@ -40,8 +40,6 @@ data class TaskCertificationUiState( return copy(torch = newFlashMode) } - fun updateCapturedImage(uri: Uri) = copy(capture = CaptureStatus.Captured(uri)) - fun updatePicture(uri: Uri): TaskCertificationUiState = copy( capture = CaptureStatus.Captured(uri), From 06d9f91a2db5ff8c34f21b2df88e26a54813220e Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 4 Feb 2026 10:49:23 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=205=EA=B8=80=EC=9E=90=20=EC=B4=88=EA=B3=BC=EC=8B=9C?= =?UTF-8?q?=20early=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/components/comment/model/CommentUiModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 1fa0ad5..8690672 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 @@ -20,7 +20,10 @@ data class CommentUiModel( comment.text.isNotEmpty() && hasMaxCommentLength - fun updateComment(comment: TextFieldValue) = copy(comment = comment.copy(comment.text.take(COMMENT_COUNT))) + fun updateComment(newComment: TextFieldValue): CommentUiModel { + if (comment.text.length > COMMENT_COUNT) return this + return copy(comment = newComment) + } fun updateFocus(isFocused: Boolean) = copy(isFocused = isFocused)