Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e3d4191
[Feat/#8] 베이직 필드
doyeon0307 Jan 10, 2026
bcf7af1
[Feat/#8] 롱/쇼트 텍스트 필드
doyeon0307 Jan 10, 2026
8d217df
[Feat/#8] 카운트 필드
doyeon0307 Jan 10, 2026
bc232a8
[Feat/#8] 커스텀 드롭다운메뉴
doyeon0307 Jan 10, 2026
b79d25e
[Feat/#8] 드롭다운 필드
doyeon0307 Jan 10, 2026
cd74801
[Fix/#8] 베이직 필드 포커스아웃 시 말줄임 텍스트 표시 로직 수정
doyeon0307 Jan 10, 2026
0904355
[Feat/#8] 서치 필드
doyeon0307 Jan 10, 2026
f8fe6be
[Chore/#8] kdoc 주석 추가 및 klint 적용
doyeon0307 Jan 10, 2026
d13dfa9
[Fix/#8] 롱 텍스트 필드 키보드 액션 디폴트를 기본값으로 변경
doyeon0307 Jan 10, 2026
3869aa3
[Fix/#8] 하드코딩 제거 및 파라미터값 전달
doyeon0307 Jan 11, 2026
48ba8af
[Fix/#8] disabled인 메뉴 아이템 터치 이벤트 차단
doyeon0307 Jan 11, 2026
898ff9d
[Fix/#8] 드롭다운 open 상태에 따른 arrow 아이콘 수정
doyeon0307 Jan 11, 2026
822c0d5
[Feat/#8] 드롭다운 메뉴 최대 높이 기본값 및 타입 설정
doyeon0307 Jan 11, 2026
00106e8
[Fix/#8] 서치 필드 포커스 보더 컬러 로직 수정
doyeon0307 Jan 11, 2026
853dd67
[Feat/#8] 에러 케이스 및 라벨 추가
doyeon0307 Jan 11, 2026
c32c57f
[Chore/#8] klint 적용
doyeon0307 Jan 11, 2026
eb17a11
[Fix/#8] 불필요한 딜레이 제거
doyeon0307 Jan 11, 2026
e20ccf7
[Chore/#8] 주석 수정
doyeon0307 Jan 11, 2026
ee16ef6
[Merge/#8] develop 병합
doyeon0307 Jan 13, 2026
54f1cb0
[Refactor/#19] modifier 익스텐션 적용
doyeon0307 Jan 13, 2026
77bc749
[Fix/#19] 오탈자 수정
doyeon0307 Jan 13, 2026
9c45c0b
[Refactor/#19] 드롭다운/서치 필드 리팩토링
doyeon0307 Jan 13, 2026
75a928c
[Refactor/#19] 필드 리팩토링
doyeon0307 Jan 13, 2026
d3779b9
[Refactor/#19] interactionSource 파라미터 추가
doyeon0307 Jan 13, 2026
09edfa6
[Chore/#19] klint 적용
doyeon0307 Jan 13, 2026
df427b0
[Chore/#19] 오탈자 수정
doyeon0307 Jan 13, 2026
c6c4f33
[Merge/#19] develop 병합
doyeon0307 Jan 13, 2026
0d641c0
[Merge/#8] develop 병합
doyeon0307 Jan 13, 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
Expand Down Expand Up @@ -136,3 +137,24 @@ fun Modifier.topRoundedBorder(
style = Stroke(width = strokeWidthPx),
)
}

fun Modifier.bottomBorder(
strokeWidth: Dp,
color: Color,
isVisible: Boolean = true,
): Modifier {
if (!isVisible) return this

return this.drawBehind {
val strokeWidthPx = strokeWidth.toPx()
val width = size.width
val height = size.height - strokeWidthPx / 2

drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width, y = height),
strokeWidth = strokeWidthPx,
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.poti.android.core.designsystem.component.field

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.poti.android.core.designsystem.theme.PotiTheme

// TODO: [도연] Display>ErrorMessage 병합 시 삭제
@Composable
fun FieldErrorMessage(
error: String,
modifier: Modifier = Modifier,
) {
if (error.isNotEmpty()) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = error,
color = PotiTheme.colors.sementicRed,
style = PotiTheme.typography.body14m,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.poti.android.core.designsystem.component.field

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.poti.android.core.designsystem.theme.PotiTheme

@Composable
internal fun FieldLabel(
label: String,
) {
if (label.isNotEmpty()) {
Text(
text = label,
color = PotiTheme.colors.black,
style = PotiTheme.typography.body14sb,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.poti.android.core.designsystem.component.field

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.poti.android.core.common.extension.roundedBackgroundWithBorder
import com.poti.android.core.designsystem.theme.PotiTheme

@Composable
internal fun PotiBasicField(
value: String,
onValueChanged: (String) -> Unit,
placeholder: String,
borderColor: Color,
backgroundColor: Color,
modifier: Modifier = Modifier,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Done,
onDoneAction: () -> Unit = {},
onNextAction: () -> Unit = {},
onSearchAction: () -> Unit = {},
onFocusChanged: (Boolean) -> Unit = {},
focusRequester: FocusRequester? = null,
singleLine: Boolean = true,
trailingIcon: @Composable () -> Unit = {},
enabled: Boolean = true,
) {
var isFocused by remember { mutableStateOf(false) }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p1: 상태를 컴퍼저블 내부에서 가지고 있으면 안될거 같아요.

interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
를 파라미터로 받고,
val isFocused by interactionSource.collectIsFocusedAsState() 를 사용하는건 어떨까요?

이렇게 하면 부모 컴포저블에서 상태관리도 가능하면서, 기존 기능 유지할 수 있을거 같아요!

만약 적용한다면, 다른 Field 도 동일할 것 같습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

움 왜 안되는지 이유도 함께 적어주심 감사드립니다!

  • PotiBasicFieldisFocused의 역할
    • singleLine일 때 focus out 시 말줄임 텍스트 노출해달라는 디자인 요구사항을 위함
  • 래핑 컴포넌트 (PotiCountField 등) 내 포커스 관리 필요할 시
    • 필요에 따라 isFocused 생성해 사용 (예: borderColor 관리)

PotiBasicFieldisFocused는 모든 singleLine 컴포넌트에 적용되는 내용이며 자체적으로 가진 포커스 여부에 따라 제어되는 부분이어서, 부모 컴포저블에서 상태관리하지 않아도 괜찮다는 것이 제 생각입니다!

BasicField는 부모 컴포저블이 관리하는 컴포넌트이기 보다는, 최소한의 로직과 기본 레이아웃을 보장하는 컴포넌트로서 이를 래핑해 유연하게 사용하는 목적으로 설계했어요.

val requester = remember {
focusRequester ?: FocusRequester()
}

BasicTextField(
value = value,
onValueChange = onValueChanged,
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.roundedBackgroundWithBorder(
cornerRadius = 8.dp,
backgroundColor = backgroundColor,
borderColor = borderColor,
borderWidth = 1.dp,
)
.focusRequester(requester)
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
onFocusChanged(focusState.isFocused)
},
singleLine = singleLine,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = imeAction,
),
keyboardActions = KeyboardActions(
onDone = { onDoneAction() },
onSearch = { onSearchAction() },
onNext = { onNextAction() },
),
enabled = enabled,
textStyle = PotiTheme.typography.body16m.copy(
color = PotiTheme.colors.black,
),
decorationBox = { innerTextField ->
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = if (singleLine) Alignment.CenterVertically else Alignment.Top,
) {
Box(
modifier = Modifier
.weight(1f),
contentAlignment = Alignment.CenterStart,
) {
innerTextField()

if (value.isEmpty()) {
Text(
text = placeholder,
color = PotiTheme.colors.gray700,
style = PotiTheme.typography.body16m,
)
}

if (singleLine && !isFocused && value.isNotEmpty()) {
Text(
text = value,
modifier = Modifier
.background(backgroundColor),
color = PotiTheme.colors.black,
style = PotiTheme.typography.body16m,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
trailingIcon()
}
},
)
}

enum class FieldStatus {
DEFAULT,
FOCUS,
ERROR,
}

val FieldStatus.borderColor: Color
@Composable get() = when (this) {
FieldStatus.DEFAULT -> PotiTheme.colors.gray300
FieldStatus.FOCUS -> PotiTheme.colors.gray700
FieldStatus.ERROR -> PotiTheme.colors.sementicRed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.poti.android.core.designsystem.component.field

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
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.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.poti.android.core.designsystem.theme.PotiTheme

/**
* 우측에 글자 수 카운트가 표시되는 텍스트 필드입니다.
* 최대 한 줄로 노출되며, 길게 작성된 경우 포커스 아웃 시 말줄임 처리 됩니다.
* maxLength는 UI 표시 용이며, 내부에 글자 수를 제어하는 로직은 없으므로 글자 수 제한은 외부에서 제어해 value로 넣어줍니다.
*
* @param value 필드 입력값입니다.
* @param onValueChanged 필드에 입력된 값을 전달합니다.
* @param placeholder 입력값이 없을 때 표시됩니다.
* @param maxLength 최대 글자 수로 표시됩니다.
* @param modifier
* @param keyboardType 키보드 입력 타입으로, 기본값은 Text입니다.
* @param label 필드 상단에 표시됩니다.
* @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다.
* @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next로 변경 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다.
* @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다.
* @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다.
*
* @author 도연
* @sample PotiCountFieldWithLabelPreview
*/
@Composable
fun PotiCountField(
value: String,
onValueChanged: (String) -> Unit,
placeholder: String,
maxLength: Int,
modifier: Modifier = Modifier,
keyboardType: KeyboardType = KeyboardType.Text,
label: String = "",
error: String = "",
imeAction: ImeAction = ImeAction.Done,
focusRequester: FocusRequester? = null,
enabled: Boolean = true,
) {
var isFocused by remember { mutableStateOf(false) }

val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current

val status = when {
error.isNotEmpty() -> FieldStatus.ERROR
isFocused -> FieldStatus.FOCUS
else -> FieldStatus.DEFAULT
}

Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
FieldLabel(label)

PotiBasicField(
value = value,
onValueChanged = onValueChanged,
placeholder = placeholder,
borderColor = status.borderColor,
backgroundColor = PotiTheme.colors.white,
modifier = Modifier
.fillMaxWidth()
.heightIn(52.dp),
keyboardType = keyboardType,
imeAction = imeAction,
onDoneAction = {
keyboardController?.hide()
focusManager.clearFocus()
},
onNextAction = {
focusManager.moveFocus(FocusDirection.Down)
},
onFocusChanged = { isFocused = it },
focusRequester = focusRequester,
singleLine = true,
trailingIcon = {
Text(
text = "${value.length}/$maxLength",
modifier = Modifier.padding(bottom = 3.dp),
color = PotiTheme.colors.gray700,
style = PotiTheme.typography.body14m,
)
},
enabled = enabled,
)

// TODO: [도연] Display>ErrorMessage으로 대체
FieldErrorMessage(error)
}
}

@Preview
@Composable
private fun PotiCountFieldWithErrorPreview() {
var text by remember { mutableStateOf("") }

PotiTheme {
PotiCountField(
value = text,
onValueChanged = { text = it },
placeholder = "플레이스홀더",
error = "에러 메시지",
maxLength = 10,
)
}
}

@Preview
@Composable
private fun PotiCountFieldWithLabelPreview() {
var text by remember { mutableStateOf("") }

PotiTheme {
PotiCountField(
value = text,
onValueChanged = { text = it },
placeholder = "플레이스홀더",
label = "라벨",
maxLength = 10,
)
}
}
Loading