-
Notifications
You must be signed in to change notification settings - Fork 1
코멘트 작성 및 예외처리 기능 추가 #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/tast-certification-gallery-pick
Are you sure you want to change the base?
Changes from all commits
303927b
0f13b3c
361f852
15383db
4f84457
d4b1a2f
ed3a8c4
b4f79d3
7fc00bb
5507d0f
33bba6c
b1afa3e
097d920
b0e828d
1ca5785
06d9f91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| 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.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 | ||
| 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, | ||
| onGuideTextPositioned: (Rect) -> Unit, | ||
| onTextFieldPositioned: (Rect) -> 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, | ||
| modifier = | ||
| Modifier.onGloballyPositioned { | ||
| onGuideTextPositioned(it.boundsInRoot()) | ||
| }, | ||
| ) | ||
|
|
||
| Spacer(modifier = Modifier.height(8.dp)) | ||
| } | ||
| CommentTextField( | ||
| uiModel = uiModel, | ||
| onCommentChanged = onCommentChanged, | ||
| onFocusChanged = onFocusChanged, | ||
| onPositioned = onTextFieldPositioned, | ||
| modifier = | ||
| Modifier | ||
| .padding(bottom = 20.dp), | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 코드를 봤을 때는 일단 텍스트 필드 포커스를 키보드 + isFocused로 관리하는 로직을 개선하면 좋을 거 같아요. keyboardAsState랑 isFocused가 활용되는 방식을 봤을 때 구현 의도는
저는 이렇게 세가지로 파악했습니다! 근데 지금 포커스랑 키보드 관리하는 로직이 여기저기 흩어져있고 복잡하게 얽혀있어서 버그가 생겼을 때 디버깅이 조금 어려울 거 같아요. 제가 잠깐 테스트해봤는데 텍스트 필드 활성화하고 해제하는 과정에서 깜빡이는 현상이 종종 발생하고 있어요 일단 지금 포커스랑 키보드 관리하는 LaunchedEffect 두개는 다 없애도 괜찮을 거 같아요. 그리고 CommentUiModel에 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| 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.LaunchedEffect | ||
| 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.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 | ||
| 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.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 | ||
|
|
||
| val CIRCLE_PADDING_START: Dp = 50.dp | ||
| val CIRCLE_SIZE: Dp = 64.dp | ||
| private val CIRCLE_GAP: Dp = CIRCLE_PADDING_START - CIRCLE_SIZE | ||
chanho0908 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Composable | ||
| fun CommentTextField( | ||
| uiModel: CommentUiModel, | ||
| onCommentChanged: (TextFieldValue) -> Unit, | ||
| onFocusChanged: (Boolean) -> Unit, | ||
| onPositioned: (Rect) -> 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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드는 없어도 포커스가 잘 잡히고 있어요! |
||
| } else { | ||
| keyboardController?.hide() | ||
| } | ||
| } | ||
|
|
||
| Box( | ||
| modifier = | ||
| modifier | ||
| .onGloballyPositioned { coordinates -> | ||
| onPositioned(coordinates.boundsInRoot()) | ||
| }.clickable( | ||
| // TODO : noClickableRipple 로 수정 | ||
| interactionSource = interactionSource, | ||
| indication = null, | ||
| onClick = { 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 = GrayColor.C500, | ||
| 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))) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드가 커서를 계속 뒤로 보내고 있어서 텍스트 중간을 수정하는 게 안되고 있어요! 그리고 스페이스바 길게 눌러서 커서 강제로 옮기면 커서가 사라지는 버그가 있습니다 |
||
| }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @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 }, | ||
| onPositioned = {}, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| 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(""), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 텍스트필드에 입력되는 변수를 뷰모델이 관리하는 전체 상태 변수에 넣으면 키보드로 값을 입력할 때마다 전체 화면이 리컴포지션이 발생해요 그래서 가급적이면 텍스트 필드 변수는 로컬 상태 변수로 처리하고 콜백으로 뷰모델에 넘기는 게 좋을 거 같은데 어떻게 생각하시나요?? |
||
| 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 | ||
|
Comment on lines
+19
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 하면 코멘트가 비어있어도 업로드가 가능할 거 같은데 요구사항이 코멘트 필수 아니었나요??? |
||
|
|
||
| fun updateComment(newComment: TextFieldValue): CommentUiModel { | ||
| if (comment.text.length > COMMENT_COUNT) return this | ||
| return copy(comment = newComment) | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.