Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
303927b
✨ Feat: 코멘트 작성을 위한 원형 텍스트 필드 구현
chanho0908 Jan 31, 2026
0f13b3c
✨ Feat: 인증샷 화면에 코멘트 작성 UI 구현
chanho0908 Jan 31, 2026
361f852
✨ Feat: 코멘트 작성 가이드 텍스트 추가
chanho0908 Jan 31, 2026
15383db
✨ Feat: 키보드 열림/닫힘 상태를 관찰하는 함수 추가
chanho0908 Jan 31, 2026
4f84457
🧹 chore: viewModel 이미지 업로드 메서드 이름 수정
chanho0908 Jan 31, 2026
d4b1a2f
✨ Feat: 사진 업로드시 코멘트 입력창에 포커싱 기능 추가
chanho0908 Jan 31, 2026
ed3a8c4
♻️ Refactor: 코멘트 가이드 텍스트 컴포저블을 commentTextfield와 통합
chanho0908 Jan 31, 2026
b4f79d3
✨ Feat: 코멘트가 유효하지 않을시 가이드 컴포저블 추가
chanho0908 Jan 31, 2026
7fc00bb
✨ Feat: 코멘트 텍스트 필드 선택시 Dimmed 효과 적용
chanho0908 Feb 1, 2026
5507d0f
✨Merge branch 'feat/tast-certification-gallery-pick' into feat/#38-ta…
chanho0908 Feb 1, 2026
33bba6c
✨ Feat: enableEdgeToEdge 적용
chanho0908 Feb 1, 2026
b1afa3e
♻️ Refactor: 누락된 Spacer 추가
chanho0908 Feb 1, 2026
097d920
♻️ Refactor: 코멘트 에러메시지 출력 시간 상수화
chanho0908 Feb 1, 2026
b0e828d
✨Merge branch 'feat/tast-certification-gallery-pick' into feat/#38-ta…
chanho0908 Feb 3, 2026
1ca5785
♻️ Refactor: 구버전 메서드를 사용하는 코드 수정
chanho0908 Feb 4, 2026
06d9f91
✨Feat: 코멘트 5글자 초과시 early return
chanho0908 Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/src/main/java/com/yapp/twix/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ToastManager>()
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true

TwixTheme {
Box(
modifier =
Modifier
.safeContentPadding()
.fillMaxSize(),
) {
AppNavHost()
Expand Down
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)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 코드를 봤을 때는 일단 텍스트 필드 포커스를 키보드 + isFocused로 관리하는 로직을 개선하면 좋을 거 같아요.

keyboardAsState랑 isFocused가 활용되는 방식을 봤을 때 구현 의도는

  1. 사진이 선택되면 자동으로 포커스 활성화
  2. 키보드가 내려가면 자동으로 포커스 해제
  3. 키보드 상태에 따라 DimmedScreen 렌더링

저는 이렇게 세가지로 파악했습니다! 근데 지금 포커스랑 키보드 관리하는 로직이 여기저기 흩어져있고 복잡하게 얽혀있어서 버그가 생겼을 때 디버깅이 조금 어려울 거 같아요. 제가 잠깐 테스트해봤는데 텍스트 필드 활성화하고 해제하는 과정에서 깜빡이는 현상이 종종 발생하고 있어요

일단 지금 포커스랑 키보드 관리하는 LaunchedEffect 두개는 다 없애도 괜찮을 거 같아요. 그리고 CommentUiModel에 focusRequestToken: Long 이거 추가해서 사진이 선택되는 이벤트가 발생할 때 업데이트해주고 CommentTextField에서 이 값을 감지해서 텍스트 필드 활성화하면 될 것 같습니다. 그리고 CommentTextField랑 DimmedScreen이 공유하는 로컬 상태 하나 추가해서 onFocusChanged 기반으로 둘의 싱크를 맞추는 것도 가능해요. 그리고 지금 키보드 옵션에 onDone 추가하셨던데 KeyboardActions에서 onDone 콜백으로 키보드 내려갈 때 공유 상태 제어해서 DimmedScreen이랑 텍스트 필드 조절하면 됩니다!

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

@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()
Copy link
Member

Choose a reason for hiding this comment

The 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)))
Copy link
Member

Choose a reason for hiding this comment

The 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(""),
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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
}
}
Loading