Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PC-434] 가치관 talk ui 구현 #43

Merged
merged 9 commits into from
Feb 1, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
Expand Down Expand Up @@ -129,7 +130,7 @@ fun PieceTextInputLong(
onValueChange = { input ->
limit?.let { if (input.length <= limit) onValueChange(input) } ?: onValueChange(input)
},
singleLine = true,
singleLine = false,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
sksowk156 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -148,19 +149,22 @@ fun PieceTextInputLong(
textStyle = PieceTheme.typography.bodyMM,
cursorBrush = SolidColor(PieceTheme.colors.primaryDefault),
decorationBox = { innerTextField ->
val isCharCountVisible: Boolean = limit != null && !readOnly
Box {
if (value.isEmpty()) {
Text(
text = hint,
style = PieceTheme.typography.bodyMM,
color = PieceTheme.colors.dark3,
modifier = Modifier.align(Alignment.TopStart)
)
}
Box(modifier = Modifier.padding(bottom = if (isCharCountVisible) 36.dp else 0.dp)) {
if (value.isEmpty()) {
Text(
text = hint,
style = PieceTheme.typography.bodyMM,
color = PieceTheme.colors.dark3,
modifier = Modifier.align(Alignment.TopStart)
)
}

innerTextField()
innerTextField()
}

if (limit != null && isFocused && value.isNotEmpty()) {
if (isCharCountVisible) {
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) {
Expand All @@ -169,7 +173,9 @@ fun PieceTextInputLong(
append("/${limit}")
},
style = PieceTheme.typography.bodySM,
modifier = Modifier.align(Alignment.BottomEnd),
modifier = Modifier
.align(Alignment.BottomEnd)
.height(20.dp),
)
}
}
Expand All @@ -178,10 +184,7 @@ fun PieceTextInputLong(
.onFocusChanged { focusState -> isFocused = focusState.isFocused }
.heightIn(min = 160.dp)
.clip(RoundedCornerShape(8.dp))
.background(
if (readOnly) PieceTheme.colors.light2
else PieceTheme.colors.light3
)
.background(PieceTheme.colors.light3)
.padding(horizontal = 16.dp, vertical = 14.dp),
)
}
Expand All @@ -190,64 +193,110 @@ fun PieceTextInputLong(
fun PieceTextInputAI(
value: String,
onValueChange: (String) -> Unit,
onSaveClick: (String) -> Unit,
modifier: Modifier = Modifier,
throttleTime: Long = 2000L,
readOnly: Boolean = true,
onDone: () -> Unit = {},
) {
val keyboardController = LocalSoftwareKeyboardController.current
var lastDoneTime by remember { mutableLongStateOf(0L) }
var isFocused by remember { mutableStateOf(false) }
var isReadOnly: Boolean by remember { mutableStateOf(readOnly) }
var isLoading: Boolean by remember { mutableStateOf(value.isBlank()) }

BasicTextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoneTime >= throttleTime) {
keyboardController?.hide()
onDone()
lastDoneTime = currentTime
Column(
modifier = modifier
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoneTime >= throttleTime) {
keyboardController?.hide()
onDone()
lastDoneTime = currentTime
}
}
}
),
textStyle = PieceTheme.typography.bodyMM,
cursorBrush = SolidColor(PieceTheme.colors.primaryDefault),
decorationBox = { innerTextField ->
Box {
if (value.isEmpty()) {
Text(
text = "작성해주신 내용을 AI가 요약하고 있어요",
style = PieceTheme.typography.bodyMM,
color = PieceTheme.colors.dark3,
modifier = Modifier.align(Alignment.CenterStart)
),
textStyle = PieceTheme.typography.bodyMM,
cursorBrush = SolidColor(PieceTheme.colors.primaryDefault),
readOnly = isReadOnly,
decorationBox = { innerTextField ->
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (isLoading) {
Text(
text = "작성해주신 내용을 AI가 요약하고 있어요",
style = PieceTheme.typography.bodyMM,
color = PieceTheme.colors.dark3,
)
} else {
innerTextField()
}

val imageRes = if (isLoading) {
R.drawable.ic_textinput_3dots
} else {
if (isReadOnly) {
R.drawable.ic_textinput_pencil
} else {
R.drawable.ic_textinput_check
}
}

Image(
painter = painterResource(imageRes),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.clickable {
if (isLoading) return@clickable

if (isReadOnly) {
isReadOnly = false
} else {
isReadOnly = true
onSaveClick(value)
}
},
)
}
},
modifier = Modifier
.onFocusChanged { focusState -> isFocused = focusState.isFocused }
.height(52.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(PieceTheme.colors.primaryLight)
.padding(horizontal = 16.dp, vertical = 14.dp),
)

innerTextField()
if (!isReadOnly) {
Row {
Spacer(modifier = Modifier.weight(1f))

val imageRes = if (value.isEmpty()) R.drawable.ic_textinput_3dots
else if (isFocused) R.drawable.ic_textinput_check
else R.drawable.ic_textinput_pencil
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(color = PieceTheme.colors.primaryDefault)) {
append(value.length.toString())
}

Image(
painter = painterResource(imageRes),
contentDescription = null,
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterEnd),
append("/20")
},
style = PieceTheme.typography.bodySR,
color = PieceTheme.colors.dark3,
modifier = Modifier.padding(top = 4.dp)
)
}
},
modifier = modifier
.onFocusChanged { focusState -> isFocused = focusState.isFocused }
.height(52.dp)
.clip(RoundedCornerShape(8.dp))
.background(PieceTheme.colors.primaryLight)
.padding(horizontal = 16.dp, vertical = 14.dp),
)
}
}
}

@Composable
Expand Down Expand Up @@ -408,6 +457,18 @@ private fun PreviewPieceTextInputAI() {
PieceTextInputAI(
value = "",
onValueChange = {},
onSaveClick = {},
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)

PieceTextInputAI(
value = "Label",
onValueChange = { },
onSaveClick = { },
readOnly = true,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
Expand All @@ -416,6 +477,8 @@ private fun PreviewPieceTextInputAI() {
PieceTextInputAI(
value = "Label",
onValueChange = { },
onSaveClick = { },
readOnly = false,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ data class PieceTypography(
fontSize = 14.sp,
lineHeight = 20.sp,
),
val bodySR: TextStyle = TextStyle(
fontFamily = PretendardRegular,
fontSize = 14.sp,
lineHeight = 20.sp,
),
val captionM: TextStyle = TextStyle(
fontFamily = PretendardMedium,
fontSize = 12.sp,
Expand Down
23 changes: 23 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.999,7.333m-0.667,0a0.667,0.667 0,1 1,1.333 0a0.667,0.667 0,1 1,-1.333 0"
android:fillColor="#1B1A2A" />
<path
android:pathData="M10,9.332L10,13.332"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:strokeColor="#1B1A2A"
android:strokeLineCap="round" />
<path
android:pathData="M10.001,10.001m-6.667,0a6.667,6.667 0,1 1,13.333 0a6.667,6.667 0,1 1,-13.333 0"
android:strokeLineJoin="round"
android:strokeWidth="1.2"
android:fillColor="#00000000"
android:strokeColor="#1B1A2A"
android:strokeLineCap="round" />
</vector>
8 changes: 8 additions & 0 deletions core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@
<string name="main_profile_value_talk_content">꿈과 목표, 관심사와 취향, 연애에 관련된\n내 생각을 확인하고 수정할 수 있습니다.</string>
<string name="main_profile_value_pick_title">가치관 Pick</string>
<string name="main_profile_value_pick_content">퀴즈를 통해 나의 연애 스타일을 파악해보고\n선택한 답변을 수정할 수 있습니다.</string>

<!--Value Talk-->
<string name="value_talk_profile_topbar_title">가치관 Talk</string>
<string name="value_talk_edit_profile_topbar_title">가치관 Talk 수정</string>
<string name="value_talk_profile_topbar_save">저장</string>
<string name="value_talk_profile_topbar_edit">수정</string>
<string name="value_talk_profile_aisummary_title">AI 요약</string>
<string name="value_talk_profile_helpmessage_title">도움말</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ data class ValueTalk(
val label: String = "",
val title: String = "",
val content: String = "",
val aiSummary: String = "",
val helpMessages: List<String> = emptyList(),
)
5 changes: 4 additions & 1 deletion core/navigation/src/main/java/com/puzzle/navigation/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@ sealed class ProfileGraphDest : Route {
data object RegisterProfileRoute : Route

@Serializable
data object ProfileRoute : Route
data object MainProfileRoute : Route

@Serializable
data object ValueTalkProfileRoute : Route
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.puzzle.common.ui.repeatOnStarted
import com.puzzle.designsystem.R
import com.puzzle.designsystem.component.PieceMainTopBar
import com.puzzle.designsystem.foundation.PieceTheme
import com.puzzle.profile.graph.main.contract.MainProfileIntent
import com.puzzle.profile.graph.main.contract.MainProfileSideEffect
import com.puzzle.profile.graph.main.contract.MainProfileState

Expand All @@ -60,7 +61,7 @@ internal fun MainProfileRoute(
MainProfileScreen(
state = state,
onMyProfileClick = {},
onValueTalkClick = {},
onValueTalkClick = { viewModel.onIntent(MainProfileIntent.OnValueTalkClick) },
onValuePickClick = {},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.hilt.AssistedViewModelFactory
import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory
import com.puzzle.domain.model.error.ErrorHelper
import com.puzzle.navigation.NavigationEvent
import com.puzzle.navigation.NavigationHelper
import com.puzzle.navigation.ProfileGraphDest
import com.puzzle.profile.graph.main.contract.MainProfileIntent
import com.puzzle.profile.graph.main.contract.MainProfileSideEffect
import com.puzzle.profile.graph.main.contract.MainProfileState
Expand Down Expand Up @@ -42,9 +44,16 @@ class MainProfileViewModel @AssistedInject constructor(
private suspend fun processIntent(intent: MainProfileIntent) {
when (intent) {
is MainProfileIntent.Navigate -> _sideEffects.send(MainProfileSideEffect.Navigate(intent.navigationEvent))
MainProfileIntent.OnValueTalkClick -> moveToValueTalkScreen()
}
}

private suspend fun moveToValueTalkScreen() {
_sideEffects.send(
MainProfileSideEffect.Navigate(NavigationEvent.NavigateTo(ProfileGraphDest.ValueTalkProfileRoute))
)
}

@AssistedFactory
interface Factory : AssistedViewModelFactory<MainProfileViewModel, MainProfileState> {
override fun create(state: MainProfileState): MainProfileViewModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import com.puzzle.navigation.NavigationEvent

sealed class MainProfileIntent {
data class Navigate(val navigationEvent: NavigationEvent) : MainProfileIntent()
data object OnValueTalkClick : MainProfileIntent()
}
Loading