From e3d4191110ff9ec511ce3885099638ae7852b5ea Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 13:56:48 +0900 Subject: [PATCH 01/25] =?UTF-8?q?[Feat/#8]=20=EB=B2=A0=EC=9D=B4=EC=A7=81?= =?UTF-8?q?=20=ED=95=84=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/DummyFieldComponent.kt | 1 - .../component/field/PotiBasicField.kt | 117 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) delete mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/DummyFieldComponent.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/DummyFieldComponent.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/DummyFieldComponent.kt deleted file mode 100644 index 970e11c3..00000000 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/DummyFieldComponent.kt +++ /dev/null @@ -1 +0,0 @@ -package com.poti.android.core.designsystem.component.field diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt new file mode 100644 index 00000000..c3b675bc --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -0,0 +1,117 @@ +package com.poti.android.core.designsystem.component.field + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.designsystem.theme.PotiTheme + +@Composable +internal fun PotiBasicField( + value: String, + onValueChaged: (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 = FocusRequester(), + singleLine: Boolean = true, + trailingIcon: @Composable () -> Unit = {}, + enabled: Boolean = true, +) { + var isFocused by remember { mutableStateOf(false) } + + BasicTextField( + value = value, + onValueChange = onValueChaged, + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .border(1.dp, borderColor, RoundedCornerShape(8.dp)) + .background(backgroundColor) + .focusRequester(focusRequester) + .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, + ) { + when { + singleLine && !isFocused && value.isNotEmpty() -> { + Text( + text = value, + color = PotiTheme.colors.black, + style = PotiTheme.typography.body16m, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + else -> { + innerTextField() + } + } + + if (value.isEmpty()) { + Text( + text = placeholder, + color = PotiTheme.colors.gray700, + style = PotiTheme.typography.body16m, + ) + } + } + trailingIcon() + } + }, + ) +} From bcf7af1ef770d899170d7296a21d2fd87b61fa0b Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 14:05:52 +0900 Subject: [PATCH 02/25] =?UTF-8?q?[Feat/#8]=20=EB=A1=B1/=EC=87=BC=ED=8A=B8?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/FieldErrorMessage.kt | 28 ++++ .../component/field/FieldLabel.kt | 18 +++ .../component/field/PotiLongTextField.kt | 120 +++++++++++++++++ .../component/field/PotiShortTextField.kt | 125 ++++++++++++++++++ 4 files changed, 291 insertions(+) create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/FieldLabel.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt new file mode 100644 index 00000000..611039e9 --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldLabel.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldLabel.kt new file mode 100644 index 00000000..437d7b92 --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldLabel.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt new file mode 100644 index 00000000..f2759dce --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt @@ -0,0 +1,120 @@ +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.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.core.designsystem.theme.PotiTheme + +/** + * Long 타입 텍스트 필드입니다. 최소 높이 160이며, 컨텍츠에 따라 높이가 늘어납니다. + * + * @param value 필드 입력값입니다. + * @param onValueChanged 필드에 입력된 값을 전달합니다. + * @param placeholder 입력값이 없을 때 표시됩니다. + * @param modifier + * @param label 필드 상단에 표시됩니다. + * @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다. + * @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next 설정 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. + * @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다. + * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. + */ +@Composable +fun PotiLongTextField( + value: String, + onValueChanged: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + label: String = "", + error: String = "", + imeAction: ImeAction = ImeAction.Done, + focusRequester: FocusRequester = remember { FocusRequester() }, + enabled: Boolean = true, +) { + var isFocused by remember { mutableStateOf(false) } + val potiColors = PotiTheme.colors + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + val borderColor = when { + error.isNotEmpty() -> potiColors.sementicRed + isFocused -> potiColors.gray700 + else -> potiColors.gray300 + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + FieldLabel(label) + + PotiBasicField( + value = value, + onValueChaged = onValueChanged, + placeholder = placeholder, + borderColor = borderColor, + backgroundColor = PotiTheme.colors.white, + modifier = Modifier + .fillMaxWidth() + .heightIn(160.dp), + enabled = enabled, + imeAction = imeAction, + onDoneAction = { + keyboardController?.hide() + focusManager.clearFocus() + }, + onNextAction = { + focusManager.moveFocus(FocusDirection.Down) + }, + onFocusChanged = { isFocused = it }, + focusRequester = focusRequester, + singleLine = false, + ) + + FieldErrorMessage(error) + } +} + +@Preview +@Composable +private fun PotiShortTextFieldWithErrorPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiLongTextField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + error = "에러 메시지", + ) + } +} + +@Preview +@Composable +private fun PotiShortTextFieldWithLabelPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiLongTextField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + label = "라벨", + ) + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt new file mode 100644 index 00000000..88a8d910 --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt @@ -0,0 +1,125 @@ +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.height +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 + +/** + * Short 타입 텍스트 필드입니다. + * 최대 한 줄로 노출되며, 길게 작성된 경우 포커스 아웃 시 말줄임 처리 됩니다. + * + * @param value 필드 입력값입니다. + * @param onValueChanged 필드에 입력된 값을 전달합니다. + * @param placeholder 입력값이 없을 때 표시됩니다. + * @param modifier + * @param keyboardType 키보드 입력 타입으로, 기본값은 Text입니다. + * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. + * @param label 필드 상단에 표시됩니다. + * @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다. + * @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next 설정 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. + * @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다. + */ +@Composable +fun PotiShortTextField( + value: String, + onValueChanged: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + keyboardType: KeyboardType = KeyboardType.Text, + enabled: Boolean = true, + label: String = "", + error: String = "", + imeAction: ImeAction = ImeAction.Done, + focusRequester: FocusRequester = remember { FocusRequester() }, +) { + var isFocused by remember { mutableStateOf(false) } + val potiColors = PotiTheme.colors + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + val borderColor = when { + error.isNotEmpty() -> potiColors.sementicRed + isFocused -> potiColors.gray700 + else -> potiColors.gray300 + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + FieldLabel(label) + + PotiBasicField( + value = value, + onValueChaged = onValueChanged, + placeholder = placeholder, + borderColor = borderColor, + backgroundColor = PotiTheme.colors.white, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + keyboardType = keyboardType, + enabled = enabled, + imeAction = imeAction, + onDoneAction = { + keyboardController?.hide() + focusManager.clearFocus() + }, + onNextAction = { + focusManager.moveFocus(FocusDirection.Down) + }, + onFocusChanged = { isFocused = it }, + focusRequester = focusRequester, + singleLine = true, + ) + + FieldErrorMessage(error) + } +} + +@Preview +@Composable +private fun PotiShortTextFieldWithErrorPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiShortTextField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + error = text, + ) + } +} + +@Preview +@Composable +private fun PotiShortTextFieldWithLabelPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiShortTextField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + label = "라벨", + ) + } +} From 8d217dfbfd20132ac49e15d26f3a5e0c33ff93e2 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 15:29:39 +0900 Subject: [PATCH 03/25] =?UTF-8?q?[Feat/#8]=20=EC=B9=B4=EC=9A=B4=ED=8A=B8?= =?UTF-8?q?=20=ED=95=84=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiCountField.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt new file mode 100644 index 00000000..fa475909 --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -0,0 +1,144 @@ +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.height +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 PotiCountFieldWithLabelPreveiw + */ +@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 = remember { FocusRequester() }, + enabled: Boolean = true, +) { + var isFocused by remember { mutableStateOf(false) } + val potiColors = PotiTheme.colors + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + val borderColor = when { + error.isNotEmpty() -> potiColors.sementicRed + isFocused -> potiColors.gray700 + else -> potiColors.gray300 + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + FieldLabel(label) + + PotiBasicField( + value = value, + onValueChaged = onValueChanged, + placeholder = placeholder, + borderColor = borderColor, + backgroundColor = PotiTheme.colors.white, + modifier = Modifier + .fillMaxWidth() + .height(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 = true, + ) + + // TODO: [도연] Display>ErrorMessage으로 대체 + FieldErrorMessage(error) + } +} + +@Preview +@Composable +private fun PotiCountFieldWithErrorPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiCountField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + error = "에러 메시지", + maxLength = 10 + ) + } +} + +@Preview +@Composable +private fun PotiCountFieldWithLabelPreveiw() { + var text by remember { mutableStateOf("") } + + PotiTheme { + PotiCountField( + value = text, + onValueChanged = { text = it }, + placeholder = "플레이스홀더", + label = "라벨", + maxLength = 10 + ) + } +} From bc232a88f38559a2522219c668f2e5e84cd1efa1 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 15:50:57 +0900 Subject: [PATCH 04/25] =?UTF-8?q?[Feat/#8]=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=EB=A9=94=EB=89=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiDropdownMenu.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt new file mode 100644 index 00000000..a77db45d --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt @@ -0,0 +1,125 @@ +package com.poti.android.core.designsystem.component.field + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +@Composable +internal fun PotiDropdownMenu( + expandedState: MutableTransitionState, + onDismissRequest: () -> Unit, + parentWidth: Int, + offset: DpOffset, + scrollState: ScrollState, + shape: Shape, + border: BorderStroke, + maxHeight: Dp?, + popupProterties: PopupProperties = PopupProperties(), + content: @Composable ColumnScope.() -> Unit, +) { + if (expandedState.currentState || expandedState.targetState) { + val density = LocalDensity.current + val popupPositionProvider = remember(offset, density) { + val offsetYPx = with(density) { offset.y.roundToPx() } + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + return IntOffset( + x = anchorBounds.left, + y = anchorBounds.bottom + offsetYPx + ) + } + } + } + + Popup( + onDismissRequest = onDismissRequest, + popupPositionProvider = popupPositionProvider, + properties = popupProterties, + ) { + PotiDropdownMenuContent( + expandedState = expandedState, + scrollState = scrollState, + shape = shape, + border = border, + parentWidth = parentWidth, + maxHeight = maxHeight, + content = content, + ) + } + } +} + +@Composable +private fun PotiDropdownMenuContent( + expandedState: MutableTransitionState, + scrollState: ScrollState, + shape: Shape, + border: BorderStroke?, + parentWidth: Int, + maxHeight: Dp?, + content: @Composable ColumnScope.() -> Unit, +) { + val density = LocalDensity.current + + AnimatedVisibility( + visibleState = expandedState, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(120, easing = LinearOutSlowInEasing) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(75, easing = LinearOutSlowInEasing) + ) + ) { + Surface( + modifier = Modifier + .width(with(density) { parentWidth.toDp() }), + shape = shape, + border = border, + ) { + Column( + modifier = + Modifier + .then( + when (maxHeight) { + null -> Modifier + else -> Modifier.heightIn(max = maxHeight) + }, + ) + .verticalScroll(scrollState), + content = content, + ) + } + } +} From b79d25e70e996b40e4d0df5b1d96146bfc19edce Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 15:51:19 +0900 Subject: [PATCH 05/25] =?UTF-8?q?[Feat/#8]=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=ED=95=84=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiDropdownField.kt | 247 ++++++++++++++++++ .../component/field/PotiMenuItem.kt | 65 +++++ .../core/designsystem/model/FieldMenuItem.kt | 10 + 3 files changed, 322 insertions(+) create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt create mode 100644 app/src/main/java/com/poti/android/core/designsystem/model/FieldMenuItem.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt new file mode 100644 index 00000000..fe5605a7 --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -0,0 +1,247 @@ +package com.poti.android.core.designsystem.component.field + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateSetOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import com.poti.android.R +import com.poti.android.core.designsystem.model.FieldMenuItem +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.core.designsystem.theme.White + +/** + * 필드 하단에 드롭다운 메뉴가 제공되는 컴포넌트입니다. + * 필드에는 텍스트 입력이 불가하며, 필드 터치로 메뉴가 여닫힙니다. + * + * @param value 필드에 표시되는 값으로, 유저가 선택한 옵션을 넣어줍니다. + * @param placeholder 선택한 옵션이 없을 때 필드에 표시됩니다. + * @param onItemClick 메뉴에서 아이템 클릭 시 호출되는 콜백으로, 클릭한 아이템 객체를 전달합니다. + * @param menuItems 메뉴에 노출되는 아이템 데이터 리스트입니다. + * @param selectedIds 메뉴 아이템의 selected 상태 표시에 쓰이며, 외부에서 제어합니다. 선택된 아이템 객체의 id 프로퍼티를 넣어줍니다. + * @param modifier + * @param initialOpenState 메뉴의 초기 열림 상태를 설정합니다. 기본값 false로, 열린 상태를 초기값으로 하고 싶을 때에만 true로 설정합니다. + * @param closeOnItemClick 메뉴 아이템 클릭 시 메뉴를 닫는 옵션입니다. 기본값 true로, 다중 선택 필요하다면 false로 설정합니다. + * @param maxHeight 메뉴 아이템이 많은 경우를 대비해 메뉴 최대 높이를 제한할 수 있습니다. 기본값 null로, 미입력 시 모든 아이템 최대로 노출됩니다. + * @param scrollState 메뉴 스크롤을 외부에서 제어하고 싶을 때 사용합니다. + * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. + * @param shape 메뉴 전체 모양입니다. + * @param border 메뉴 전체 테두리입니다. + * + * @author 도연 + * @sample PotiDropdownFieldPreveiw + */ +@Composable +fun PotiDropdownField( + value: String, + placeholder: String, + onItemClick: (FieldMenuItem) -> Unit, + menuItems: List, + selectedIds: Set, + modifier: Modifier = Modifier, + initialOpenState: Boolean = false, + closeOnItemClick: Boolean = true, + maxHeight: Dp? = null, + scrollState: ScrollState = rememberScrollState(), + offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), + shape: Shape = RoundedCornerShape(8.dp), + border: BorderStroke = BorderStroke(1.dp, PotiTheme.colors.gray700), +) { + val expandedState = remember { MutableTransitionState(false) } + var parentWidth by remember { mutableIntStateOf(0) } + + LaunchedEffect(Unit) { + if (initialOpenState) { + expandedState.targetState = true + } + } + + Box( + modifier = modifier + .fillMaxWidth(), + ) { + PotiBasicField( + value = value, + onValueChaged = {}, + placeholder = placeholder, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .onGloballyPositioned { coordinates -> + parentWidth = coordinates.size.width + } + .onFocusChanged { focusState -> + if (!focusState.isFocused && expandedState.currentState) { + expandedState.targetState = false + } + } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + expandedState.targetState = !expandedState.currentState + }, + borderColor = when { + expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 + else -> PotiTheme.colors.gray300 + }, + backgroundColor = White, + trailingIcon = { + Crossfade( + targetState = expandedState.targetState, + ) { opened -> + Icon( + imageVector = ImageVector.vectorResource(if (opened) R.drawable.ic_arrow_down_lg else R.drawable.ic_arrow_up_lg), + contentDescription = null, + modifier = Modifier + .size(24.dp), + tint = PotiTheme.colors.gray700, + ) + } + }, + enabled = false, + ) + + PotiDropdownMenu( + expandedState = expandedState, + onDismissRequest = { + expandedState.targetState = false + }, + scrollState = scrollState, + parentWidth = parentWidth, + offset = offset, + shape = shape, + border = border, + maxHeight = maxHeight, + popupProterties = PopupProperties( + focusable = true, + ), + ) { + menuItems.forEachIndexed { index, item -> + PotiMenuItem( + option = item.option, + onClick = { + onItemClick(item) + if (closeOnItemClick) { + expandedState.targetState = false + } + }, + isSelected = item.id in selectedIds, + price = item.price, + disabled = item.disabled, + ) + + if (index < menuItems.lastIndex) { + // TODO: [도연] Display>Divider-sm로 변경 + HorizontalDivider( + thickness = 1.dp, + color = PotiTheme.colors.gray300, + ) + } + } + } + } +} + +@Preview +@Composable +private fun PotiDropdownFieldPreveiw() { + var text by remember { mutableStateOf("") } + val selectedIds = remember { mutableStateSetOf() } + val menuItems = listOf( + FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), + ) + + PotiTheme { + PotiDropdownField( + value = text, + placeholder = "플레이스 홀더", + initialOpenState = true, + onItemClick = { + if (it.id in selectedIds) { + selectedIds.remove(it.id) + text = "" + } else { + selectedIds.clear() + selectedIds.add(it.id) + text = it.option + } + }, + menuItems = menuItems, + selectedIds = selectedIds, + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 40.dp), + maxHeight = 600.dp, + ) + } +} + +@Preview +@Composable +private fun PotiDropdownFieldWithPriceWithMutlipleSelectPreveiw() { + var text by remember { mutableStateOf("") } + val selectedIds = remember { mutableStateSetOf() } + val menuItems = listOf( + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + FieldMenuItem("옵션", "1,000원"), + ) + + PotiTheme { + PotiDropdownField( + value = text, + placeholder = "플레이스 홀더", + initialOpenState = true, + onItemClick = { + if (it.id in selectedIds) { + selectedIds.remove(it.id) + text = "" + } else { + selectedIds.add(it.id) + text = it.option + } + }, + menuItems = menuItems, + selectedIds = selectedIds, + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 40.dp), + closeOnItemClick = false, + ) + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt new file mode 100644 index 00000000..2051374f --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt @@ -0,0 +1,65 @@ +package com.poti.android.core.designsystem.component.field + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.poti.android.core.designsystem.theme.PotiTheme + +@Composable +fun PotiMenuItem( + option: String, + onClick: () -> Unit, + isSelected: Boolean, + modifier: Modifier = Modifier, + price: String? = null, + disabled: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val backgroundColor = when { + isPressed -> PotiTheme.colors.gray100 + else -> PotiTheme.colors.white + } + + val textColor = when { + isSelected || disabled -> PotiTheme.colors.gray700 + else -> PotiTheme.colors.black + } + + Row( + modifier = modifier + .background(backgroundColor) + .clickable( + interactionSource = interactionSource, + indication = null, + ) { onClick() } + .padding(horizontal = 16.dp, vertical = 15.5.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = option, + color = textColor, + style = PotiTheme.typography.body14m, + modifier = Modifier.weight(1f), + ) + + if (price != null) { + Text( + text = price, + color = textColor, + style = PotiTheme.typography.body14m, + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/model/FieldMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/model/FieldMenuItem.kt new file mode 100644 index 00000000..5bc6c6ab --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/model/FieldMenuItem.kt @@ -0,0 +1,10 @@ +package com.poti.android.core.designsystem.model + +import java.util.UUID + +data class FieldMenuItem( + val option: String, + val price: String? = null, + val disabled: Boolean = false, + val id: String = UUID.randomUUID().toString(), +) From cd74801864d7c263858ae01f9340e2dddc3f64fd Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 16:07:10 +0900 Subject: [PATCH 06/25] =?UTF-8?q?[Fix/#8]=20=EB=B2=A0=EC=9D=B4=EC=A7=81=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EB=A7=90=EC=A4=84=EC=9E=84=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20as-is:innerTextField()=EA=B0=80=20?= =?UTF-8?q?=EB=A7=90=EC=A4=84=EC=9E=84=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=EC=B2=B4=EB=90=98=EC=96=B4,=20=EC=9E=AC?= =?UTF-8?q?=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EC=8B=9C=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EA=B0=80=20=EB=A7=A8=20=EC=95=9E=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=90=A8=20to-be:innerT?= =?UTF-8?q?extField()=EB=8A=94=20=ED=95=AD=EC=8B=9C=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=EB=A7=90=EC=A4=84=EC=9E=84=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94?= =?UTF-8?q?=EC=9D=84=20=EC=8A=A4=ED=83=9D=EC=9C=BC=EB=A1=9C=20=EC=8C=93?= =?UTF-8?q?=EC=95=84=20=EC=BB=A4=EC=84=9C=20=EC=9C=84=EC=B9=98=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiBasicField.kt | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index c3b675bc..f7fcd216 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -86,21 +86,7 @@ internal fun PotiBasicField( .weight(1f), contentAlignment = Alignment.CenterStart, ) { - when { - singleLine && !isFocused && value.isNotEmpty() -> { - Text( - text = value, - color = PotiTheme.colors.black, - style = PotiTheme.typography.body16m, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - else -> { - innerTextField() - } - } + innerTextField() if (value.isEmpty()) { Text( @@ -109,6 +95,18 @@ internal fun PotiBasicField( 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() } From 090435529140d0a3134aaef1212c02f3968e7c54 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 17:50:43 +0900 Subject: [PATCH 07/25] =?UTF-8?q?[Feat/#8]=20=EC=84=9C=EC=B9=98=20?= =?UTF-8?q?=ED=95=84=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiSearchField.kt | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt new file mode 100644 index 00000000..a814694a --- /dev/null +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -0,0 +1,234 @@ +package com.poti.android.core.designsystem.component.field + +import android.util.Log +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateSetOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.designsystem.model.FieldMenuItem +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.core.designsystem.theme.White +import kotlinx.coroutines.delay + +@Composable +fun PotiSearchField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + onSearchClick: (String) -> Unit, + onItemClick: (FieldMenuItem) -> Unit, + menuItems: List, + selectedIds: Set, + modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, + scrollState: ScrollState = rememberScrollState(), + offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), + shape: Shape = RoundedCornerShape(8.dp), + border: BorderStroke = BorderStroke(1.dp, PotiTheme.colors.gray700), + maxHeight: Dp? = null, +) { + val expandedState = remember { MutableTransitionState(false) } + var parentWidth by remember { mutableIntStateOf(0) } + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + var isFieldFocused by remember { mutableStateOf(false) } + var searchActionDone by remember { mutableStateOf(false) } + var dismissRequestDone by remember { mutableStateOf(false) } + var isTyping by remember { mutableStateOf(false) } + + fun onSearch() { + onSearchClick(value) + searchActionDone = true + Log.d("Search", "👾 searchActionDone=$searchActionDone 세팅 완료") + } + + fun clearFocusAndHideKeyboard() { + focusManager.clearFocus() + keyboardController?.hide() + } + + LaunchedEffect(isFieldFocused, menuItems.size) { + if (isFieldFocused) { + Log.d("Search", "🛸 포커스 있거나 메뉴 길이가 바뀌었다") + delay(100) + Log.d("Search", "🛸 잠깐 기다렸어") + expandedState.targetState = menuItems.isNotEmpty() + Log.d("Search", "🛸 메뉴 길이에 따라 ${expandedState.targetState}로 설정 완") + } + } + + LaunchedEffect(dismissRequestDone) { + if (dismissRequestDone) { + Log.d("Search", "👽 dismissRequestDone이어서ㅡLaunchedEffect 실헹") + delay(100) + Log.d("Search", "👽 잠깐 기다렸어") + if (searchActionDone || isTyping) { + searchActionDone = false + isTyping = false + Log.d("Search", "👽 search/typing 했대서 암것도 안 했어") + } else { + expandedState.targetState = false + clearFocusAndHideKeyboard() + Log.d("Search", "👽 암 것도 아니니까 다 닫았따") + } + dismissRequestDone = false + } + } + + Box( + modifier = modifier + .fillMaxWidth(), + ) { + PotiBasicField( + value = value, + onValueChaged = { + isTyping = true + Log.d("Search", "🦝 키보드 입력") + onValueChange(it) + }, + placeholder = placeholder, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .onGloballyPositioned { coordinates -> + parentWidth = coordinates.size.width + }, + onFocusChanged = { isFieldFocused = it }, + borderColor = when { + expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 + else -> PotiTheme.colors.gray300 + }, + backgroundColor = White, + imeAction = ImeAction.Search, + onSearchAction = { onSearch() }, + trailingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_search), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { onSearch() }, + tint = PotiTheme.colors.gray700, + ) + }, + focusRequester = focusRequester, + ) + + PotiDropdownMenu( + expandedState = expandedState, + onDismissRequest = { + Log.d("Search", "🚨 onDismissRequest 호출됨!") + Log.d("Search", "🚨 현재 포커스: $isFieldFocused") + dismissRequestDone = true + }, + scrollState = scrollState, + offset = offset, + shape = shape, + border = border, + maxHeight = maxHeight, + parentWidth = parentWidth + ) { + menuItems.forEachIndexed { index, item -> + PotiMenuItem( + option = item.option, + onClick = { + onItemClick(item) + expandedState.targetState = false + clearFocusAndHideKeyboard() + }, + isSelected = item.id in selectedIds, + modifier = Modifier.fillMaxWidth(), + price = item.price, + disabled = item.disabled, + ) + + if (index < menuItems.lastIndex) { + // TODO: [도연] Display>Divider-sm로 변경 + HorizontalDivider( + thickness = 1.dp, + color = PotiTheme.colors.gray300, + ) + } + } + } + } +} + +@Preview +@Composable +private fun PotiSearchFieldPreview() { + var text by remember { mutableStateOf("") } + val selectedIds = remember { mutableStateSetOf() } + val menuItems = remember { mutableStateListOf() } + + PotiTheme { + PotiSearchField( + value = text, + onValueChange = { + text = it + menuItems.clear() + if (text == "hih") { + menuItems.add(FieldMenuItem("hih")) + } else { + menuItems.clear() + } + }, + placeholder = "플레이스홀더", + onItemClick = { + if (it.id in selectedIds) { + selectedIds.remove(it.id) + text = "" + } else { + selectedIds.clear() + selectedIds.add(it.id) + text = it.option + } + }, + menuItems = menuItems, + selectedIds = selectedIds, + modifier = Modifier + .padding(horizontal = 20.dp) + .padding(top = 40.dp), + maxHeight = 600.dp, + onSearchClick = { }, + ) + } +} From f8fe6be824dcfefec0d377e9a5436fca1f9afc87 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 18:02:47 +0900 Subject: [PATCH 08/25] =?UTF-8?q?[Chore/#8]=20kdoc=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20klint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiCountField.kt | 4 +-- .../component/field/PotiDropdownMenu.kt | 8 ++--- .../component/field/PotiSearchField.kt | 36 +++++++++++-------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt index fa475909..a9565882 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -122,7 +122,7 @@ private fun PotiCountFieldWithErrorPreveiw() { onValueChanged = { text = it }, placeholder = "플레이스홀더", error = "에러 메시지", - maxLength = 10 + maxLength = 10, ) } } @@ -138,7 +138,7 @@ private fun PotiCountFieldWithLabelPreveiw() { onValueChanged = { text = it }, placeholder = "플레이스홀더", label = "라벨", - maxLength = 10 + maxLength = 10, ) } } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt index a77db45d..5bbea785 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt @@ -55,7 +55,7 @@ internal fun PotiDropdownMenu( ): IntOffset { return IntOffset( x = anchorBounds.left, - y = anchorBounds.bottom + offsetYPx + y = anchorBounds.bottom + offsetYPx, ) } } @@ -95,12 +95,12 @@ private fun PotiDropdownMenuContent( visibleState = expandedState, enter = slideInVertically( initialOffsetY = { -it }, - animationSpec = tween(120, easing = LinearOutSlowInEasing) + animationSpec = tween(120, easing = LinearOutSlowInEasing), ), exit = slideOutVertically( targetOffsetY = { -it }, - animationSpec = tween(75, easing = LinearOutSlowInEasing) - ) + animationSpec = tween(75, easing = LinearOutSlowInEasing), + ), ) { Surface( modifier = Modifier diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index a814694a..5054e1c8 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -1,6 +1,5 @@ package com.poti.android.core.designsystem.component.field -import android.util.Log import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ScrollState @@ -43,6 +42,26 @@ import com.poti.android.core.designsystem.theme.PotiTheme import com.poti.android.core.designsystem.theme.White import kotlinx.coroutines.delay +/** + * 필드에 검색어 입력 시, 검색 결과가 드롭다운 메뉴로 제공되는 컴포넌트입니다. + * 입력에 따른 자동 검색인 경우 외부에서 적절한 menuItems를 넣어주며 제어하며, 명시적인 검색 콜백은 onSearchClick으로 전달합니다. + * + * + * @param value 필드 입력값입니다. 메뉴에서 유저가 아이템 선택 시 선택한 옵션으로 대체합니다. + * @param onValueChange 필드에 입력된 값을 전달합니다. + * @param placeholder 입력값 및 선택한 옵션이 없ㅇ르 때 필드에 표시됩니다. + * @param onSearchClick 검색 콜백입니다. + * @param onItemClick 메뉴에서 아이템 클릭 시 호출되는 콜백으로, 클릭한 아이템 객체를 전달합니다. + * @param menuItems 메뉴에 노출되는 아이템 데이터 리스트로, 검색 결과를 넣어줍니다. + * @param selectedIds 메뉴 아이템의 selected 상태 표시에 쓰이며, 외부에서 제어합니다. 선택된 아이템 객체의 id 프로퍼티를 넣어줍니다. + * @param modifier + * @param maxHeight 메뉴 아이템이 많은 경우를 대비해 메뉴 최대 높이를 제한할 수 있습니다. 기본값 null로, 미입력 시 모든 아이템 최대로 노출됩니다. + * @param focusRequester 포커스를 외부에서 제어하고 싶을 때 사용합니다. 예: 화면 진입 시 필드에 포커스 가도록 + * @param scrollState 메뉴 스크롤을 외부에서 제어하고 싶을 때 사용합니다. + * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. + * @param shape 메뉴 전체 모양입니다. + * @param border 메뉴 전체 테두리입니다. + */ @Composable fun PotiSearchField( value: String, @@ -53,12 +72,12 @@ fun PotiSearchField( menuItems: List, selectedIds: Set, modifier: Modifier = Modifier, + maxHeight: Dp? = null, focusRequester: FocusRequester = remember { FocusRequester() }, scrollState: ScrollState = rememberScrollState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), shape: Shape = RoundedCornerShape(8.dp), border: BorderStroke = BorderStroke(1.dp, PotiTheme.colors.gray700), - maxHeight: Dp? = null, ) { val expandedState = remember { MutableTransitionState(false) } var parentWidth by remember { mutableIntStateOf(0) } @@ -74,7 +93,6 @@ fun PotiSearchField( fun onSearch() { onSearchClick(value) searchActionDone = true - Log.d("Search", "👾 searchActionDone=$searchActionDone 세팅 완료") } fun clearFocusAndHideKeyboard() { @@ -84,27 +102,20 @@ fun PotiSearchField( LaunchedEffect(isFieldFocused, menuItems.size) { if (isFieldFocused) { - Log.d("Search", "🛸 포커스 있거나 메뉴 길이가 바뀌었다") delay(100) - Log.d("Search", "🛸 잠깐 기다렸어") expandedState.targetState = menuItems.isNotEmpty() - Log.d("Search", "🛸 메뉴 길이에 따라 ${expandedState.targetState}로 설정 완") } } LaunchedEffect(dismissRequestDone) { if (dismissRequestDone) { - Log.d("Search", "👽 dismissRequestDone이어서ㅡLaunchedEffect 실헹") delay(100) - Log.d("Search", "👽 잠깐 기다렸어") if (searchActionDone || isTyping) { searchActionDone = false isTyping = false - Log.d("Search", "👽 search/typing 했대서 암것도 안 했어") } else { expandedState.targetState = false clearFocusAndHideKeyboard() - Log.d("Search", "👽 암 것도 아니니까 다 닫았따") } dismissRequestDone = false } @@ -118,7 +129,6 @@ fun PotiSearchField( value = value, onValueChaged = { isTyping = true - Log.d("Search", "🦝 키보드 입력") onValueChange(it) }, placeholder = placeholder, @@ -155,8 +165,6 @@ fun PotiSearchField( PotiDropdownMenu( expandedState = expandedState, onDismissRequest = { - Log.d("Search", "🚨 onDismissRequest 호출됨!") - Log.d("Search", "🚨 현재 포커스: $isFieldFocused") dismissRequestDone = true }, scrollState = scrollState, @@ -164,7 +172,7 @@ fun PotiSearchField( shape = shape, border = border, maxHeight = maxHeight, - parentWidth = parentWidth + parentWidth = parentWidth, ) { menuItems.forEachIndexed { index, item -> PotiMenuItem( From d13dfa9fddd00cdde58b88058cb733cf406381e6 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 10 Jan 2026 18:54:34 +0900 Subject: [PATCH 09/25] =?UTF-8?q?[Fix/#8]=20=EB=A1=B1=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=95=84=EB=93=9C=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EB=94=94=ED=8F=B4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiLongTextField.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt index f2759dce..fb0600dd 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt @@ -28,7 +28,7 @@ import com.poti.android.core.designsystem.theme.PotiTheme * @param modifier * @param label 필드 상단에 표시됩니다. * @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다. - * @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next 설정 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. + * @param imeAction 키보드 액션 타입으로, 기본값은 Default 입니다. Done 설정 시 키보드 닫힘, Next 설정 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. * @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다. * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. */ @@ -40,7 +40,7 @@ fun PotiLongTextField( modifier: Modifier = Modifier, label: String = "", error: String = "", - imeAction: ImeAction = ImeAction.Done, + imeAction: ImeAction = ImeAction.Default, focusRequester: FocusRequester = remember { FocusRequester() }, enabled: Boolean = true, ) { From 3869aa3174811febacb08b50d2c399f6c2021606 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:03:58 +0900 Subject: [PATCH 10/25] =?UTF-8?q?[Fix/#8]=20=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EA=B0=92=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/designsystem/component/field/PotiCountField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt index a9565882..25b40049 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -103,7 +103,7 @@ fun PotiCountField( style = PotiTheme.typography.body14m, ) }, - enabled = true, + enabled = enabled, ) // TODO: [도연] Display>ErrorMessage으로 대체 From 48ba8af6f9d64fc69416b03ecd4870730f188592 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:04:24 +0900 Subject: [PATCH 11/25] =?UTF-8?q?[Fix/#8]=20disabled=EC=9D=B8=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=95=84=EC=9D=B4=ED=85=9C=20=ED=84=B0=EC=B9=98=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiMenuItem.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt index 2051374f..37b12f88 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt @@ -28,7 +28,7 @@ fun PotiMenuItem( val isPressed by interactionSource.collectIsPressedAsState() val backgroundColor = when { - isPressed -> PotiTheme.colors.gray100 + !disabled && isPressed -> PotiTheme.colors.gray100 else -> PotiTheme.colors.white } @@ -43,7 +43,9 @@ fun PotiMenuItem( .clickable( interactionSource = interactionSource, indication = null, - ) { onClick() } + enabled = !disabled, + onClick = onClick + ) .padding(horizontal = 16.dp, vertical = 15.5.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { From 898ff9d3d064f5fb80ebc944e5e44877cb766c9d Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:04:44 +0900 Subject: [PATCH 12/25] =?UTF-8?q?[Fix/#8]=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20open=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20arrow=20=EC=95=84=EC=9D=B4=EC=BD=98=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 --- .../core/designsystem/component/field/PotiDropdownField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index fe5605a7..7ac88765 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -120,7 +120,7 @@ fun PotiDropdownField( targetState = expandedState.targetState, ) { opened -> Icon( - imageVector = ImageVector.vectorResource(if (opened) R.drawable.ic_arrow_down_lg else R.drawable.ic_arrow_up_lg), + imageVector = ImageVector.vectorResource(if (opened) R.drawable.ic_arrow_up_lg else R.drawable.ic_arrow_down_lg), contentDescription = null, modifier = Modifier .size(24.dp), From 822c0d5400fe9f710c553584226bfd7e70c4d235 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:24:57 +0900 Subject: [PATCH 13/25] =?UTF-8?q?[Feat/#8]=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=A9=94=EB=89=B4=20=EC=B5=9C=EB=8C=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EB=B0=8F=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiDropdownField.kt | 4 ++-- .../designsystem/component/field/PotiSearchField.kt | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 7ac88765..5ba44f86 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -51,7 +51,7 @@ import com.poti.android.core.designsystem.theme.White * @param modifier * @param initialOpenState 메뉴의 초기 열림 상태를 설정합니다. 기본값 false로, 열린 상태를 초기값으로 하고 싶을 때에만 true로 설정합니다. * @param closeOnItemClick 메뉴 아이템 클릭 시 메뉴를 닫는 옵션입니다. 기본값 true로, 다중 선택 필요하다면 false로 설정합니다. - * @param maxHeight 메뉴 아이템이 많은 경우를 대비해 메뉴 최대 높이를 제한할 수 있습니다. 기본값 null로, 미입력 시 모든 아이템 최대로 노출됩니다. + * @param maxHeight 메뉴 최대 높이를 제한합니다. 기본값 422dp입니다. * @param scrollState 메뉴 스크롤을 외부에서 제어하고 싶을 때 사용합니다. * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. * @param shape 메뉴 전체 모양입니다. @@ -70,7 +70,7 @@ fun PotiDropdownField( modifier: Modifier = Modifier, initialOpenState: Boolean = false, closeOnItemClick: Boolean = true, - maxHeight: Dp? = null, + maxHeight: Dp? = 422.dp, scrollState: ScrollState = rememberScrollState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), shape: Shape = RoundedCornerShape(8.dp), diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 5054e1c8..2aabb4b1 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -54,8 +54,8 @@ import kotlinx.coroutines.delay * @param onItemClick 메뉴에서 아이템 클릭 시 호출되는 콜백으로, 클릭한 아이템 객체를 전달합니다. * @param menuItems 메뉴에 노출되는 아이템 데이터 리스트로, 검색 결과를 넣어줍니다. * @param selectedIds 메뉴 아이템의 selected 상태 표시에 쓰이며, 외부에서 제어합니다. 선택된 아이템 객체의 id 프로퍼티를 넣어줍니다. + * @param searchType 서치 타입에 따라 메뉴 최대 길이가 조정됩니다. 아티스트 검색 시 ARTIST, 상품 등록을 위한 상품명 검색 시 PRODUCT를 사용합니다. * @param modifier - * @param maxHeight 메뉴 아이템이 많은 경우를 대비해 메뉴 최대 높이를 제한할 수 있습니다. 기본값 null로, 미입력 시 모든 아이템 최대로 노출됩니다. * @param focusRequester 포커스를 외부에서 제어하고 싶을 때 사용합니다. 예: 화면 진입 시 필드에 포커스 가도록 * @param scrollState 메뉴 스크롤을 외부에서 제어하고 싶을 때 사용합니다. * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. @@ -71,8 +71,8 @@ fun PotiSearchField( onItemClick: (FieldMenuItem) -> Unit, menuItems: List, selectedIds: Set, + searchType: SearchType, modifier: Modifier = Modifier, - maxHeight: Dp? = null, focusRequester: FocusRequester = remember { FocusRequester() }, scrollState: ScrollState = rememberScrollState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), @@ -171,7 +171,7 @@ fun PotiSearchField( offset = offset, shape = shape, border = border, - maxHeight = maxHeight, + maxHeight = searchType.maxHeight, parentWidth = parentWidth, ) { menuItems.forEachIndexed { index, item -> @@ -200,6 +200,11 @@ fun PotiSearchField( } } +enum class SearchType(val maxHeight: Dp) { + ARTIST(500.dp), + PRODUCT(156.dp) +} + @Preview @Composable private fun PotiSearchFieldPreview() { @@ -235,8 +240,8 @@ private fun PotiSearchFieldPreview() { modifier = Modifier .padding(horizontal = 20.dp) .padding(top = 40.dp), - maxHeight = 600.dp, onSearchClick = { }, + searchType = SearchType.ARTIST ) } } From 00106e8133f4ce68485fe9569a9c9a52faa70bd2 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:34:06 +0900 Subject: [PATCH 14/25] =?UTF-8?q?[Fix/#8]=20=EC=84=9C=EC=B9=98=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EB=B3=B4=EB=8D=94=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?as-is=20:=20=EB=A9=94=EB=89=B4=20=EC=98=A4=ED=94=88=20=EC=8B=9C?= =?UTF-8?q?=20=EB=B3=B4=EB=8D=94=20=EC=BB=AC=EB=9F=AC=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20to-be=20:=20=ED=95=84=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=20=EC=8B=9C=20=EB=B3=B4=EB=8D=94=20=EC=BB=AC=EB=9F=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiSearchField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 2aabb4b1..26858ea3 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -140,7 +140,7 @@ fun PotiSearchField( }, onFocusChanged = { isFieldFocused = it }, borderColor = when { - expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 + isFieldFocused -> PotiTheme.colors.gray700 else -> PotiTheme.colors.gray300 }, backgroundColor = White, From 853dd672d58491aed45e6402b189a45a8293254b Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:53:02 +0900 Subject: [PATCH 15/25] =?UTF-8?q?[Feat/#8]=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=EB=9D=BC=EB=B2=A8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiDropdownField.kt | 97 +++++++++++-------- .../component/field/PotiSearchField.kt | 91 ++++++++++------- 2 files changed, 113 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 5ba44f86..91a56525 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ScrollState 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.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -49,6 +51,8 @@ import com.poti.android.core.designsystem.theme.White * @param menuItems 메뉴에 노출되는 아이템 데이터 리스트입니다. * @param selectedIds 메뉴 아이템의 selected 상태 표시에 쓰이며, 외부에서 제어합니다. 선택된 아이템 객체의 id 프로퍼티를 넣어줍니다. * @param modifier + * @param label 필드 상단에 표시됩니다. + * @param error emptyString이 아닌 경우 필드 하단에 에러 메시지를 노출하고, borderColor를 변경합니다. * @param initialOpenState 메뉴의 초기 열림 상태를 설정합니다. 기본값 false로, 열린 상태를 초기값으로 하고 싶을 때에만 true로 설정합니다. * @param closeOnItemClick 메뉴 아이템 클릭 시 메뉴를 닫는 옵션입니다. 기본값 true로, 다중 선택 필요하다면 false로 설정합니다. * @param maxHeight 메뉴 최대 높이를 제한합니다. 기본값 422dp입니다. @@ -68,6 +72,8 @@ fun PotiDropdownField( menuItems: List, selectedIds: Set, modifier: Modifier = Modifier, + label: String = "", + error: String = "", initialOpenState: Boolean = false, closeOnItemClick: Boolean = true, maxHeight: Dp? = 422.dp, @@ -85,51 +91,63 @@ fun PotiDropdownField( } } + val borderColor = when { + error.isNotEmpty() -> PotiTheme.colors.sementicRed + expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 + else -> PotiTheme.colors.gray300 + } + Box( modifier = modifier .fillMaxWidth(), ) { - PotiBasicField( - value = value, - onValueChaged = {}, - placeholder = placeholder, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .onGloballyPositioned { coordinates -> - parentWidth = coordinates.size.width - } - .onFocusChanged { focusState -> - if (!focusState.isFocused && expandedState.currentState) { - expandedState.targetState = false + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + FieldLabel(label) + + PotiBasicField( + value = value, + onValueChaged = {}, + placeholder = placeholder, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .onGloballyPositioned { coordinates -> + parentWidth = coordinates.size.width + } + .onFocusChanged { focusState -> + if (!focusState.isFocused && expandedState.currentState) { + expandedState.targetState = false + } + } + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + expandedState.targetState = !expandedState.currentState + }, + borderColor = borderColor, + backgroundColor = White, + trailingIcon = { + Crossfade( + targetState = expandedState.targetState, + ) { opened -> + Icon( + imageVector = ImageVector.vectorResource(if (opened) R.drawable.ic_arrow_up_lg else R.drawable.ic_arrow_down_lg), + contentDescription = null, + modifier = Modifier + .size(24.dp), + tint = PotiTheme.colors.gray700, + ) } - } - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { - expandedState.targetState = !expandedState.currentState }, - borderColor = when { - expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 - else -> PotiTheme.colors.gray300 - }, - backgroundColor = White, - trailingIcon = { - Crossfade( - targetState = expandedState.targetState, - ) { opened -> - Icon( - imageVector = ImageVector.vectorResource(if (opened) R.drawable.ic_arrow_up_lg else R.drawable.ic_arrow_down_lg), - contentDescription = null, - modifier = Modifier - .size(24.dp), - tint = PotiTheme.colors.gray700, - ) - } - }, - enabled = false, - ) + enabled = false, + ) + + // TODO: [도연] Display>errorMessage로 대체 + FieldErrorMessage(error) + } PotiDropdownMenu( expandedState = expandedState, @@ -241,6 +259,7 @@ private fun PotiDropdownFieldWithPriceWithMutlipleSelectPreveiw() { modifier = Modifier .padding(horizontal = 20.dp) .padding(top = 40.dp), + error = "에러 메시지", closeOnItemClick = false, ) } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 26858ea3..691671b3 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ScrollState 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.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -56,6 +58,8 @@ import kotlinx.coroutines.delay * @param selectedIds 메뉴 아이템의 selected 상태 표시에 쓰이며, 외부에서 제어합니다. 선택된 아이템 객체의 id 프로퍼티를 넣어줍니다. * @param searchType 서치 타입에 따라 메뉴 최대 길이가 조정됩니다. 아티스트 검색 시 ARTIST, 상품 등록을 위한 상품명 검색 시 PRODUCT를 사용합니다. * @param modifier + * @param label 필드 상단에 표시됩니다. + * @param error emptyString이 아닌 경우 필드 하단에 에러 메시지를 노출하고, borderColor를 변경합니다. * @param focusRequester 포커스를 외부에서 제어하고 싶을 때 사용합니다. 예: 화면 진입 시 필드에 포커스 가도록 * @param scrollState 메뉴 스크롤을 외부에서 제어하고 싶을 때 사용합니다. * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. @@ -73,6 +77,8 @@ fun PotiSearchField( selectedIds: Set, searchType: SearchType, modifier: Modifier = Modifier, + label: String = "", + error: String = "", focusRequester: FocusRequester = remember { FocusRequester() }, scrollState: ScrollState = rememberScrollState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), @@ -121,46 +127,58 @@ fun PotiSearchField( } } + val borderColor = when { + error.isNotEmpty() -> PotiTheme.colors.sementicRed + expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 + else -> PotiTheme.colors.gray300 + } + Box( modifier = modifier .fillMaxWidth(), ) { - PotiBasicField( - value = value, - onValueChaged = { - isTyping = true - onValueChange(it) - }, - placeholder = placeholder, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .onGloballyPositioned { coordinates -> - parentWidth = coordinates.size.width + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + FieldLabel(label) + + PotiBasicField( + value = value, + onValueChaged = { + isTyping = true + onValueChange(it) }, - onFocusChanged = { isFieldFocused = it }, - borderColor = when { - isFieldFocused -> PotiTheme.colors.gray700 - else -> PotiTheme.colors.gray300 - }, - backgroundColor = White, - imeAction = ImeAction.Search, - onSearchAction = { onSearch() }, - trailingIcon = { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_search), - contentDescription = null, - modifier = Modifier - .size(24.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { onSearch() }, - tint = PotiTheme.colors.gray700, - ) - }, - focusRequester = focusRequester, - ) + placeholder = placeholder, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .onGloballyPositioned { coordinates -> + parentWidth = coordinates.size.width + }, + onFocusChanged = { isFieldFocused = it }, + borderColor = borderColor, + backgroundColor = White, + imeAction = ImeAction.Search, + onSearchAction = { onSearch() }, + trailingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_search), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { onSearch() }, + tint = PotiTheme.colors.gray700, + ) + }, + focusRequester = focusRequester, + ) + + // TODO: [도연] Display>errorMessage로 대체 + FieldErrorMessage(error) + } PotiDropdownMenu( expandedState = expandedState, @@ -241,7 +259,8 @@ private fun PotiSearchFieldPreview() { .padding(horizontal = 20.dp) .padding(top = 40.dp), onSearchClick = { }, - searchType = SearchType.ARTIST + searchType = SearchType.ARTIST, + error = "에러 메시지" ) } } From c32c57f623a71c192b481254b73b50dabd1e3e61 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 18:56:12 +0900 Subject: [PATCH 16/25] =?UTF-8?q?[Chore/#8]=20klint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiDropdownField.kt | 2 +- .../core/designsystem/component/field/PotiMenuItem.kt | 2 +- .../core/designsystem/component/field/PotiSearchField.kt | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 91a56525..4e93dddd 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -102,7 +102,7 @@ fun PotiDropdownField( .fillMaxWidth(), ) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), ) { FieldLabel(label) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt index 37b12f88..d5af2405 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt @@ -44,7 +44,7 @@ fun PotiMenuItem( interactionSource = interactionSource, indication = null, enabled = !disabled, - onClick = onClick + onClick = onClick, ) .padding(horizontal = 16.dp, vertical = 15.5.dp), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 691671b3..e98a15e5 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -138,7 +138,7 @@ fun PotiSearchField( .fillMaxWidth(), ) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), ) { FieldLabel(label) @@ -220,7 +220,7 @@ fun PotiSearchField( enum class SearchType(val maxHeight: Dp) { ARTIST(500.dp), - PRODUCT(156.dp) + PRODUCT(156.dp), } @Preview @@ -260,7 +260,7 @@ private fun PotiSearchFieldPreview() { .padding(top = 40.dp), onSearchClick = { }, searchType = SearchType.ARTIST, - error = "에러 메시지" + error = "에러 메시지", ) } } From eb17a117421398968cba6ea9e67e7a869b168779 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 20:09:20 +0900 Subject: [PATCH 17/25] =?UTF-8?q?[Fix/#8]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=94=9C=EB=A0=88=EC=9D=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/designsystem/component/field/PotiSearchField.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index e98a15e5..1a298cc8 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -108,7 +108,6 @@ fun PotiSearchField( LaunchedEffect(isFieldFocused, menuItems.size) { if (isFieldFocused) { - delay(100) expandedState.targetState = menuItems.isNotEmpty() } } From e20ccf77414cbf347eccf980c36532a3dc8a958e Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 11 Jan 2026 20:15:49 +0900 Subject: [PATCH 18/25] =?UTF-8?q?[Chore/#8]=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiSearchField.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 1a298cc8..a4e44c87 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -51,7 +51,7 @@ import kotlinx.coroutines.delay * * @param value 필드 입력값입니다. 메뉴에서 유저가 아이템 선택 시 선택한 옵션으로 대체합니다. * @param onValueChange 필드에 입력된 값을 전달합니다. - * @param placeholder 입력값 및 선택한 옵션이 없ㅇ르 때 필드에 표시됩니다. + * @param placeholder 입력값 및 선택한 옵션이 없을 때 필드에 표시됩니다. * @param onSearchClick 검색 콜백입니다. * @param onItemClick 메뉴에서 아이템 클릭 시 호출되는 콜백으로, 클릭한 아이템 객체를 전달합니다. * @param menuItems 메뉴에 노출되는 아이템 데이터 리스트로, 검색 결과를 넣어줍니다. @@ -65,6 +65,9 @@ import kotlinx.coroutines.delay * @param offset 필드 하단으로부터 메뉴까지의 간격입니다. 기본값 12dp이며, y값만 조정 가능합니다. * @param shape 메뉴 전체 모양입니다. * @param border 메뉴 전체 테두리입니다. + * + * @author 도연 + * @sample PotiSearchFieldPreview */ @Composable fun PotiSearchField( From 54f1cb0117acdce48a98b755bb5c3689bd878af2 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 11:37:44 +0900 Subject: [PATCH 19/25] =?UTF-8?q?[Refactor/#19]=20modifier=20=EC=9D=B5?= =?UTF-8?q?=EC=8A=A4=ED=85=90=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiBasicField.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index f7fcd216..4a516ef5 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -26,6 +26,7 @@ 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 @@ -54,8 +55,12 @@ internal fun PotiBasicField( onValueChange = onValueChaged, modifier = modifier .clip(RoundedCornerShape(8.dp)) - .border(1.dp, borderColor, RoundedCornerShape(8.dp)) - .background(backgroundColor) + .roundedBackgroundWithBorder( + cornerRadius = 8.dp, + backgroundColor = backgroundColor, + borderColor = borderColor, + borderWidth = 1.dp + ) .focusRequester(focusRequester) .onFocusChanged { focusState -> isFocused = focusState.isFocused From 77bc749f133e013768a48f0a765dc10685164b64 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 11:41:47 +0900 Subject: [PATCH 20/25] =?UTF-8?q?[Fix/#19]=20=EC=98=A4=ED=83=88=EC=9E=90?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiBasicField.kt | 5 ++--- .../core/designsystem/component/field/PotiCountField.kt | 8 ++++---- .../designsystem/component/field/PotiDropdownField.kt | 8 ++++---- .../designsystem/component/field/PotiLongTextField.kt | 6 +++--- .../core/designsystem/component/field/PotiSearchField.kt | 2 +- .../designsystem/component/field/PotiShortTextField.kt | 6 +++--- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index 4a516ef5..345b6f92 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -1,7 +1,6 @@ package com.poti.android.core.designsystem.component.field import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -32,7 +31,7 @@ import com.poti.android.core.designsystem.theme.PotiTheme @Composable internal fun PotiBasicField( value: String, - onValueChaged: (String) -> Unit, + onValueChanged: (String) -> Unit, placeholder: String, borderColor: Color, backgroundColor: Color, @@ -52,7 +51,7 @@ internal fun PotiBasicField( BasicTextField( value = value, - onValueChange = onValueChaged, + onValueChange = onValueChanged, modifier = modifier .clip(RoundedCornerShape(8.dp)) .roundedBackgroundWithBorder( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt index 25b40049..6613abde 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -40,7 +40,7 @@ import com.poti.android.core.designsystem.theme.PotiTheme * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. * * @author 도연 - * @sample PotiCountFieldWithLabelPreveiw + * @sample PotiCountFieldWithLabelPreview */ @Composable fun PotiCountField( @@ -76,7 +76,7 @@ fun PotiCountField( PotiBasicField( value = value, - onValueChaged = onValueChanged, + onValueChanged = onValueChanged, placeholder = placeholder, borderColor = borderColor, backgroundColor = PotiTheme.colors.white, @@ -113,7 +113,7 @@ fun PotiCountField( @Preview @Composable -private fun PotiCountFieldWithErrorPreveiw() { +private fun PotiCountFieldWithErrorPreview() { var text by remember { mutableStateOf("") } PotiTheme { @@ -129,7 +129,7 @@ private fun PotiCountFieldWithErrorPreveiw() { @Preview @Composable -private fun PotiCountFieldWithLabelPreveiw() { +private fun PotiCountFieldWithLabelPreview() { var text by remember { mutableStateOf("") } PotiTheme { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 4e93dddd..cce3b356 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -62,7 +62,7 @@ import com.poti.android.core.designsystem.theme.White * @param border 메뉴 전체 테두리입니다. * * @author 도연 - * @sample PotiDropdownFieldPreveiw + * @sample PotiDropdownFieldPreview */ @Composable fun PotiDropdownField( @@ -108,7 +108,7 @@ fun PotiDropdownField( PotiBasicField( value = value, - onValueChaged = {}, + onValueChanged = {}, placeholder = placeholder, modifier = Modifier .fillMaxWidth() @@ -192,7 +192,7 @@ fun PotiDropdownField( @Preview @Composable -private fun PotiDropdownFieldPreveiw() { +private fun PotiDropdownFieldPreview() { var text by remember { mutableStateOf("") } val selectedIds = remember { mutableStateSetOf() } val menuItems = listOf( @@ -226,7 +226,7 @@ private fun PotiDropdownFieldPreveiw() { @Preview @Composable -private fun PotiDropdownFieldWithPriceWithMutlipleSelectPreveiw() { +private fun PotiDropdownFieldWithPriceWithMutlipleSelectPreview() { var text by remember { mutableStateOf("") } val selectedIds = remember { mutableStateSetOf() } val menuItems = listOf( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt index fb0600dd..f2d3fd59 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt @@ -64,7 +64,7 @@ fun PotiLongTextField( PotiBasicField( value = value, - onValueChaged = onValueChanged, + onValueChanged = onValueChanged, placeholder = placeholder, borderColor = borderColor, backgroundColor = PotiTheme.colors.white, @@ -91,7 +91,7 @@ fun PotiLongTextField( @Preview @Composable -private fun PotiShortTextFieldWithErrorPreveiw() { +private fun PotiLongTextFieldWithErrorPreview() { var text by remember { mutableStateOf("") } PotiTheme { @@ -106,7 +106,7 @@ private fun PotiShortTextFieldWithErrorPreveiw() { @Preview @Composable -private fun PotiShortTextFieldWithLabelPreveiw() { +private fun PotiLongTextFieldWithLabelPreview() { var text by remember { mutableStateOf("") } PotiTheme { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index a4e44c87..f6404e5b 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -146,7 +146,7 @@ fun PotiSearchField( PotiBasicField( value = value, - onValueChaged = { + onValueChanged = { isTyping = true onValueChange(it) }, diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt index 88a8d910..c4269fa0 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt @@ -68,7 +68,7 @@ fun PotiShortTextField( PotiBasicField( value = value, - onValueChaged = onValueChanged, + onValueChanged = onValueChanged, placeholder = placeholder, borderColor = borderColor, backgroundColor = PotiTheme.colors.white, @@ -96,7 +96,7 @@ fun PotiShortTextField( @Preview @Composable -private fun PotiShortTextFieldWithErrorPreveiw() { +private fun PotiShortTextFieldWithErrorPreview() { var text by remember { mutableStateOf("") } PotiTheme { @@ -111,7 +111,7 @@ private fun PotiShortTextFieldWithErrorPreveiw() { @Preview @Composable -private fun PotiShortTextFieldWithLabelPreveiw() { +private fun PotiShortTextFieldWithLabelPreview() { var text by remember { mutableStateOf("") } PotiTheme { From 9c45c0bb2bc28744f0908a5448e877ac670bedda Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 16:37:23 +0900 Subject: [PATCH 21/25] =?UTF-8?q?[Refactor/#19]=20=EB=93=9C=EB=A1=AD?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4/=EC=84=9C=EC=B9=98=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EA=B0=84=20=EB=94=94=EB=B0=94=EC=9D=B4=EB=8D=94=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20bottomBorder=20=EC=A0=81=EC=9A=A9=20-=20?= =?UTF-8?q?=EC=84=9C=EC=B9=98=20=ED=95=84=EB=93=9C=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20-=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EB=A9=94=EB=89=B4=20=EC=B5=9C=EB=8C=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=A1=B0=EC=A0=95=20-=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=95=84=EC=9D=B4=ED=85=9C=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=20=EB=86=92=EC=9D=B4=20=EC=84=A4=EC=A0=95,=20=EB=A6=AC?= =?UTF-8?q?=ED=94=8C=20=EC=9D=B5=EC=8A=A4=ED=85=90=EC=85=98=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20Column=20->=20LazyColumn=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/common/extension/ModifierExt.kt | 22 +++++++ .../component/field/PotiDropdownField.kt | 60 +++++++++---------- .../component/field/PotiDropdownMenu.kt | 21 ++++--- .../component/field/PotiMenuItem.kt | 20 +++++-- .../component/field/PotiSearchField.kt | 19 ++---- 5 files changed, 81 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/common/extension/ModifierExt.kt b/app/src/main/java/com/poti/android/core/common/extension/ModifierExt.kt index 48377260..f69c1592 100644 --- a/app/src/main/java/com/poti/android/core/common/extension/ModifierExt.kt +++ b/app/src/main/java/com/poti/android/core/common/extension/ModifierExt.kt @@ -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 @@ -103,3 +104,24 @@ fun Modifier.dropShadow( } } } + +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, + ) + } +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index cce3b356..76b654a6 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -3,7 +3,6 @@ package com.poti.android.core.designsystem.component.field import androidx.compose.animation.Crossfade import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -13,12 +12,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -26,10 +25,10 @@ import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -39,7 +38,6 @@ import androidx.compose.ui.window.PopupProperties import com.poti.android.R import com.poti.android.core.designsystem.model.FieldMenuItem import com.poti.android.core.designsystem.theme.PotiTheme -import com.poti.android.core.designsystem.theme.White /** * 필드 하단에 드롭다운 메뉴가 제공되는 컴포넌트입니다. @@ -76,20 +74,18 @@ fun PotiDropdownField( error: String = "", initialOpenState: Boolean = false, closeOnItemClick: Boolean = true, - maxHeight: Dp? = 422.dp, - scrollState: ScrollState = rememberScrollState(), + maxHeight: Dp = 422.dp, + scrollState: LazyListState = rememberLazyListState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), shape: Shape = RoundedCornerShape(8.dp), border: BorderStroke = BorderStroke(1.dp, PotiTheme.colors.gray700), ) { - val expandedState = remember { MutableTransitionState(false) } + val expandedState = remember { MutableTransitionState(initialOpenState) } var parentWidth by remember { mutableIntStateOf(0) } - LaunchedEffect(Unit) { - if (initialOpenState) { - expandedState.targetState = true - } - } + val density = LocalDensity.current + var calculatedMaxHeight by remember { mutableStateOf(0.dp) } + var isCalculateFinished by remember { mutableStateOf(false) } val borderColor = when { error.isNotEmpty() -> PotiTheme.colors.sementicRed @@ -116,11 +112,6 @@ fun PotiDropdownField( .onGloballyPositioned { coordinates -> parentWidth = coordinates.size.width } - .onFocusChanged { focusState -> - if (!focusState.isFocused && expandedState.currentState) { - expandedState.targetState = false - } - } .clickable( indication = null, interactionSource = remember { MutableInteractionSource() }, @@ -128,7 +119,7 @@ fun PotiDropdownField( expandedState.targetState = !expandedState.currentState }, borderColor = borderColor, - backgroundColor = White, + backgroundColor = PotiTheme.colors.white, trailingIcon = { Crossfade( targetState = expandedState.targetState, @@ -159,12 +150,12 @@ fun PotiDropdownField( offset = offset, shape = shape, border = border, - maxHeight = maxHeight, + maxHeight = if (isCalculateFinished) calculatedMaxHeight else maxHeight, popupProterties = PopupProperties( focusable = true, ), ) { - menuItems.forEachIndexed { index, item -> + itemsIndexed(menuItems) { index, item -> PotiMenuItem( option = item.option, onClick = { @@ -174,17 +165,24 @@ fun PotiDropdownField( } }, isSelected = item.id in selectedIds, + modifier = when (isCalculateFinished) { + true -> Modifier + else -> + Modifier + .onGloballyPositioned { coordinates -> + val heightPx = coordinates.size.height + val heightDp = with(density) { heightPx.toDp() } + if (heightDp + calculatedMaxHeight > maxHeight) { + isCalculateFinished = true + return@onGloballyPositioned + } + calculatedMaxHeight += heightDp + } + }, price = item.price, disabled = item.disabled, + showBottomBorder = index < menuItems.size, ) - - if (index < menuItems.lastIndex) { - // TODO: [도연] Display>Divider-sm로 변경 - HorizontalDivider( - thickness = 1.dp, - color = PotiTheme.colors.gray300, - ) - } } } } @@ -196,7 +194,7 @@ private fun PotiDropdownFieldPreview() { var text by remember { mutableStateOf("") } val selectedIds = remember { mutableStateSetOf() } val menuItems = listOf( - FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), + FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션".repeat(50)), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), FieldMenuItem("옵션"), ) PotiTheme { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt index 5bbea785..6827952e 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt @@ -7,12 +7,11 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.width -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -35,12 +34,12 @@ internal fun PotiDropdownMenu( onDismissRequest: () -> Unit, parentWidth: Int, offset: DpOffset, - scrollState: ScrollState, + scrollState: LazyListState, shape: Shape, border: BorderStroke, maxHeight: Dp?, popupProterties: PopupProperties = PopupProperties(), - content: @Composable ColumnScope.() -> Unit, + content: LazyListScope.() -> Unit, ) { if (expandedState.currentState || expandedState.targetState) { val density = LocalDensity.current @@ -82,12 +81,12 @@ internal fun PotiDropdownMenu( @Composable private fun PotiDropdownMenuContent( expandedState: MutableTransitionState, - scrollState: ScrollState, + scrollState: LazyListState, shape: Shape, border: BorderStroke?, parentWidth: Int, maxHeight: Dp?, - content: @Composable ColumnScope.() -> Unit, + content: LazyListScope.() -> Unit, ) { val density = LocalDensity.current @@ -108,7 +107,8 @@ private fun PotiDropdownMenuContent( shape = shape, border = border, ) { - Column( + LazyColumn( + state = scrollState, modifier = Modifier .then( @@ -116,8 +116,7 @@ private fun PotiDropdownMenuContent( null -> Modifier else -> Modifier.heightIn(max = maxHeight) }, - ) - .verticalScroll(scrollState), + ), content = content, ) } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt index d5af2405..1a730116 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt @@ -1,11 +1,11 @@ package com.poti.android.core.designsystem.component.field import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -13,6 +13,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.poti.android.core.common.extension.bottomBorder +import com.poti.android.core.common.extension.noRippleClickable import com.poti.android.core.designsystem.theme.PotiTheme @Composable @@ -23,6 +25,7 @@ fun PotiMenuItem( modifier: Modifier = Modifier, price: String? = null, disabled: Boolean = false, + showBottomBorder: Boolean = true, ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -39,14 +42,19 @@ fun PotiMenuItem( Row( modifier = modifier + .heightIn(52.dp) .background(backgroundColor) - .clickable( - interactionSource = interactionSource, - indication = null, - enabled = !disabled, + .bottomBorder( + strokeWidth = 1.dp, + color = PotiTheme.colors.gray300, + isVisible = showBottomBorder, + ) + .noRippleClickable( onClick = onClick, + enabled = !disabled, + interactionSource = remember { MutableInteractionSource() }, ) - .padding(horizontal = 16.dp, vertical = 15.5.dp), + .padding(horizontal = 16.dp, vertical = 15.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { Text( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index f6404e5b..90c82ab0 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -2,7 +2,6 @@ package com.poti.android.core.designsystem.component.field import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -12,9 +11,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -83,7 +83,7 @@ fun PotiSearchField( label: String = "", error: String = "", focusRequester: FocusRequester = remember { FocusRequester() }, - scrollState: ScrollState = rememberScrollState(), + scrollState: LazyListState = rememberLazyListState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), shape: Shape = RoundedCornerShape(8.dp), border: BorderStroke = BorderStroke(1.dp, PotiTheme.colors.gray700), @@ -194,7 +194,7 @@ fun PotiSearchField( maxHeight = searchType.maxHeight, parentWidth = parentWidth, ) { - menuItems.forEachIndexed { index, item -> + itemsIndexed(menuItems) { index, item -> PotiMenuItem( option = item.option, onClick = { @@ -206,15 +206,8 @@ fun PotiSearchField( modifier = Modifier.fillMaxWidth(), price = item.price, disabled = item.disabled, + showBottomBorder = index < menuItems.size, ) - - if (index < menuItems.lastIndex) { - // TODO: [도연] Display>Divider-sm로 변경 - HorizontalDivider( - thickness = 1.dp, - color = PotiTheme.colors.gray300, - ) - } } } } From 75a928c4b0dde23a1c89e379cc4e45660a534473 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 17:15:20 +0900 Subject: [PATCH 22/25] =?UTF-8?q?[Refactor/#19]=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20FieldStatus=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20borderColor=20=EA=B4=80=EB=A6=AC=20-=20foc?= =?UTF-8?q?usRequester=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20null=EB=A1=9C=20?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20null=EC=9D=B8=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20PotiBasicField=20=EB=82=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/field/PotiBasicField.kt | 20 ++++++++++++++++--- .../component/field/PotiCountField.kt | 17 ++++++++-------- .../component/field/PotiDropdownField.kt | 14 ++++++------- .../component/field/PotiLongTextField.kt | 11 +++++----- .../component/field/PotiSearchField.kt | 16 +++++++-------- .../component/field/PotiShortTextField.kt | 17 ++++++++-------- 6 files changed, 53 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index 345b6f92..3d59528a 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -42,12 +42,15 @@ internal fun PotiBasicField( onNextAction: () -> Unit = {}, onSearchAction: () -> Unit = {}, onFocusChanged: (Boolean) -> Unit = {}, - focusRequester: FocusRequester = FocusRequester(), + focusRequester: FocusRequester? = null, singleLine: Boolean = true, trailingIcon: @Composable () -> Unit = {}, enabled: Boolean = true, ) { var isFocused by remember { mutableStateOf(false) } + val requester = remember { + focusRequester ?: FocusRequester() + } BasicTextField( value = value, @@ -58,9 +61,9 @@ internal fun PotiBasicField( cornerRadius = 8.dp, backgroundColor = backgroundColor, borderColor = borderColor, - borderWidth = 1.dp + borderWidth = 1.dp, ) - .focusRequester(focusRequester) + .focusRequester(requester) .onFocusChanged { focusState -> isFocused = focusState.isFocused onFocusChanged(focusState.isFocused) @@ -117,3 +120,14 @@ internal fun PotiBasicField( }, ) } + +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 + } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt index 6613abde..6ced36c0 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -3,7 +3,7 @@ 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.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -53,19 +53,18 @@ fun PotiCountField( label: String = "", error: String = "", imeAction: ImeAction = ImeAction.Done, - focusRequester: FocusRequester = remember { FocusRequester() }, + focusRequester: FocusRequester? = null, enabled: Boolean = true, ) { var isFocused by remember { mutableStateOf(false) } - val potiColors = PotiTheme.colors val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current - val borderColor = when { - error.isNotEmpty() -> potiColors.sementicRed - isFocused -> potiColors.gray700 - else -> potiColors.gray300 + val status = when { + error.isNotEmpty() -> FieldStatus.ERROR + isFocused -> FieldStatus.FOCUS + else -> FieldStatus.DEFAULT } Column( @@ -78,11 +77,11 @@ fun PotiCountField( value = value, onValueChanged = onValueChanged, placeholder = placeholder, - borderColor = borderColor, + borderColor = status.borderColor, backgroundColor = PotiTheme.colors.white, modifier = Modifier .fillMaxWidth() - .height(52.dp), + .heightIn(52.dp), keyboardType = keyboardType, imeAction = imeAction, onDoneAction = { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 76b654a6..927b341a 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListState @@ -87,10 +87,10 @@ fun PotiDropdownField( var calculatedMaxHeight by remember { mutableStateOf(0.dp) } var isCalculateFinished by remember { mutableStateOf(false) } - val borderColor = when { - error.isNotEmpty() -> PotiTheme.colors.sementicRed - expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 - else -> PotiTheme.colors.gray300 + val status = when { + error.isNotEmpty() -> FieldStatus.ERROR + expandedState.targetState -> FieldStatus.FOCUS + else -> FieldStatus.DEFAULT } Box( @@ -108,7 +108,7 @@ fun PotiDropdownField( placeholder = placeholder, modifier = Modifier .fillMaxWidth() - .height(52.dp) + .heightIn(52.dp) .onGloballyPositioned { coordinates -> parentWidth = coordinates.size.width } @@ -118,7 +118,7 @@ fun PotiDropdownField( ) { expandedState.targetState = !expandedState.currentState }, - borderColor = borderColor, + borderColor = status.borderColor, backgroundColor = PotiTheme.colors.white, trailingIcon = { Crossfade( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt index f2d3fd59..fd7dfe90 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt @@ -45,15 +45,14 @@ fun PotiLongTextField( enabled: Boolean = true, ) { var isFocused by remember { mutableStateOf(false) } - val potiColors = PotiTheme.colors val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current - val borderColor = when { - error.isNotEmpty() -> potiColors.sementicRed - isFocused -> potiColors.gray700 - else -> potiColors.gray300 + val status = when { + error.isNotEmpty() -> FieldStatus.ERROR + isFocused -> FieldStatus.FOCUS + else -> FieldStatus.DEFAULT } Column( @@ -66,7 +65,7 @@ fun PotiLongTextField( value = value, onValueChanged = onValueChanged, placeholder = placeholder, - borderColor = borderColor, + borderColor = status.borderColor, backgroundColor = PotiTheme.colors.white, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index 90c82ab0..bff7492e 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListState @@ -82,7 +82,7 @@ fun PotiSearchField( modifier: Modifier = Modifier, label: String = "", error: String = "", - focusRequester: FocusRequester = remember { FocusRequester() }, + focusRequester: FocusRequester? = null, scrollState: LazyListState = rememberLazyListState(), offset: DpOffset = DpOffset(x = 0.dp, y = 12.dp), shape: Shape = RoundedCornerShape(8.dp), @@ -129,10 +129,10 @@ fun PotiSearchField( } } - val borderColor = when { - error.isNotEmpty() -> PotiTheme.colors.sementicRed - expandedState.currentState || expandedState.targetState -> PotiTheme.colors.gray700 - else -> PotiTheme.colors.gray300 + val status = when { + error.isNotEmpty() -> FieldStatus.ERROR + isFieldFocused -> FieldStatus.FOCUS + else -> FieldStatus.DEFAULT } Box( @@ -153,12 +153,12 @@ fun PotiSearchField( placeholder = placeholder, modifier = Modifier .fillMaxWidth() - .height(52.dp) + .heightIn(52.dp) .onGloballyPositioned { coordinates -> parentWidth = coordinates.size.width }, onFocusChanged = { isFieldFocused = it }, - borderColor = borderColor, + borderColor = status.borderColor, backgroundColor = White, imeAction = ImeAction.Search, onSearchAction = { onSearch() }, diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt index c4269fa0..68ee4789 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt @@ -3,7 +3,7 @@ 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.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -46,18 +46,17 @@ fun PotiShortTextField( label: String = "", error: String = "", imeAction: ImeAction = ImeAction.Done, - focusRequester: FocusRequester = remember { FocusRequester() }, + focusRequester: FocusRequester? = null, ) { var isFocused by remember { mutableStateOf(false) } - val potiColors = PotiTheme.colors val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current - val borderColor = when { - error.isNotEmpty() -> potiColors.sementicRed - isFocused -> potiColors.gray700 - else -> potiColors.gray300 + val status = when { + error.isNotEmpty() -> FieldStatus.ERROR + isFocused -> FieldStatus.FOCUS + else -> FieldStatus.DEFAULT } Column( @@ -70,11 +69,11 @@ fun PotiShortTextField( value = value, onValueChanged = onValueChanged, placeholder = placeholder, - borderColor = borderColor, + borderColor = status.borderColor, backgroundColor = PotiTheme.colors.white, modifier = Modifier .fillMaxWidth() - .height(52.dp), + .heightIn(52.dp), keyboardType = keyboardType, enabled = enabled, imeAction = imeAction, From d3779b92ebe9b79579ce06724e023e4ebb6f0cb8 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 17:47:29 +0900 Subject: [PATCH 23/25] =?UTF-8?q?[Refactor/#19]=20interactionSource=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/designsystem/component/field/PotiMenuItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt index 1a730116..38bbb41a 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiMenuItem.kt @@ -1,6 +1,7 @@ package com.poti.android.core.designsystem.component.field import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement @@ -26,8 +27,8 @@ fun PotiMenuItem( price: String? = null, disabled: Boolean = false, showBottomBorder: Boolean = true, + interactionSource: InteractionSource = remember { MutableInteractionSource() }, ) { - val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val backgroundColor = when { From 09edfa659736d2c1c646b4afbd8c0d78290b7a17 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 17:51:16 +0900 Subject: [PATCH 24/25] =?UTF-8?q?[Chore/#19]=20klint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiBasicField.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index 3d59528a..31da0faf 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -122,7 +122,9 @@ internal fun PotiBasicField( } enum class FieldStatus { - DEFAULT, FOCUS, ERROR; + DEFAULT, + FOCUS, + ERROR, } val FieldStatus.borderColor: Color From df427b042c64bcec63312ab8bc999f67e9b6fa53 Mon Sep 17 00:00:00 2001 From: dodo Date: Tue, 13 Jan 2026 17:51:31 +0900 Subject: [PATCH 25/25] =?UTF-8?q?[Chore/#19]=20=EC=98=A4=ED=83=88=EC=9E=90?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/field/PotiDropdownField.kt | 2 +- .../core/designsystem/component/field/PotiDropdownMenu.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index 927b341a..eb691b8e 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -151,7 +151,7 @@ fun PotiDropdownField( shape = shape, border = border, maxHeight = if (isCalculateFinished) calculatedMaxHeight else maxHeight, - popupProterties = PopupProperties( + popupProperties = PopupProperties( focusable = true, ), ) { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt index 6827952e..7c9a717b 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownMenu.kt @@ -38,7 +38,7 @@ internal fun PotiDropdownMenu( shape: Shape, border: BorderStroke, maxHeight: Dp?, - popupProterties: PopupProperties = PopupProperties(), + popupProperties: PopupProperties = PopupProperties(), content: LazyListScope.() -> Unit, ) { if (expandedState.currentState || expandedState.targetState) { @@ -63,7 +63,7 @@ internal fun PotiDropdownMenu( Popup( onDismissRequest = onDismissRequest, popupPositionProvider = popupPositionProvider, - properties = popupProterties, + properties = popupProperties, ) { PotiDropdownMenuContent( expandedState = expandedState,