From 3d002f7581dc12fc514caf3616693be774522e0b Mon Sep 17 00:00:00 2001 From: dada4 Date: Tue, 21 Jul 2026 00:06:31 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/onboarding/impl/build.gradle.kts | 3 + .../kr/co/call/impl/component/MemberChoice.kt | 6 ++ .../java/kr/co/call/impl/component/NameBox.kt | 85 ++++++++++++++++ .../co/call/impl/component/ProfileChoice.kt | 68 +++++++++++++ .../kr/co/call/impl/component/TopTitle.kt | 93 ++++++++++++++++++ .../main/res/drawable/onboarding_check.png | Bin 0 -> 16956 bytes .../main/res/drawable/onboarding_profile.xml | 30 ++++++ 7 files changed, 285 insertions(+) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt create mode 100644 feature/onboarding/impl/src/main/res/drawable/onboarding_check.png create mode 100644 feature/onboarding/impl/src/main/res/drawable/onboarding_profile.xml diff --git a/feature/onboarding/impl/build.gradle.kts b/feature/onboarding/impl/build.gradle.kts index d13cdb62..30dd0c9c 100644 --- a/feature/onboarding/impl/build.gradle.kts +++ b/feature/onboarding/impl/build.gradle.kts @@ -39,4 +39,7 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + + //coil 설정 + implementation(libs.coil.compose) } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt new file mode 100644 index 00000000..649b13e2 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -0,0 +1,6 @@ +package kr.co.call.impl.component + +import androidx.compose.runtime.Composable + +@Composable +fun MemberChoice(){} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt new file mode 100644 index 00000000..51592b2f --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt @@ -0,0 +1,85 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray100 +import kr.co.call.designsystem.theme.SubRed + +@Composable +fun NameBox( + modifier: Modifier, + label:String, + value: String, + onValueChange: (String) ->Unit, + ){ + Column( + modifier=modifier + .width(125.dp) + ){ + Row { + Text(text=label) + Text(text=" *", + color= SubRed + ) + } + Spacer(modifier=Modifier.height(6.dp)) + Box( + modifier= Modifier + .fillMaxWidth() + .height(51.dp) + .background( + color=Gray100, + shape=RoundedCornerShape(10.dp) + ) + .padding(horizontal=16.dp), + contentAlignment= Alignment.CenterStart + ) { + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + textStyle = CallTheme.typography.bodyMediumMedium, + modifier = Modifier + .fillMaxWidth() + ) + } + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +@Composable +private fun NameBoxPreview() { + var name by remember { + mutableStateOf("") + } + + NameBox( + modifier = Modifier, + label = "이름", + value = name, + onValueChange = { name = it }, + ) +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt new file mode 100644 index 00000000..63c9fcca --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt @@ -0,0 +1,68 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.onboarding.impl.R + +@Composable +fun ProfileChoice( + modifier: Modifier, + imageUrl: String?, + onClick: ()->Unit, + size: Dp =99.dp +){ + Box( + modifier=modifier + .size(size) + .clickable(onClick=onClick) + ) { + if (imageUrl.isNullOrEmpty()){ + Image( + imageVector= ImageVector.vectorResource(id= R.drawable.onboarding_profile), + contentDescription="기본 프로필 이미지", + modifier=Modifier.fillMaxSize(), + contentScale= ContentScale.Fit + ) + } else{ + AsyncImage( + model=imageUrl, + contentDescription="선택된 프로필 사진", + modifier=Modifier + .fillMaxSize() + .clip(CircleShape), + contentScale=ContentScale.Crop + ) + } + } +} + +@Preview( + name = "기본 프로필 이미지", + showBackground = true +) +@Composable +private fun ProfileChoicePreview() { + CallFromAiTheme { + ProfileChoice( + modifier = Modifier.padding(20.dp), + imageUrl = null, + onClick = {} + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt new file mode 100644 index 00000000..64f839d0 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt @@ -0,0 +1,93 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.MainVariant2 + +@Composable +fun TopTitle( + modifier: Modifier= Modifier, + title: String, + description: String, + currentStep: Int?=null, + totalStep: Int?=null, + ){ + Column( + modifier = modifier.fillMaxWidth(), + ){ + Row( + modifier= Modifier + .fillMaxWidth() + .padding(end=14.dp), + verticalAlignment=Alignment.CenterVertically, + ){ + Text( + text=title, + style=CallTheme.typography.titleMediumBold, + color=Black, + modifier=Modifier.weight(1f, fill=true), + ) + + if (currentStep != null && totalStep != null){ + StepNumber(currentStep=currentStep, totalStep = totalStep) + } + } + Spacer(modifier=Modifier.height(3.dp)) + Text( + text=description, + style= CallTheme.typography.bodySmall, + color= Gray400, + ) + } +} + +@Composable +fun StepNumber( + currentStep: Int, + totalStep: Int, +){ + Text( + text="$currentStep/$totalStep", + style= CallTheme.typography.bodyMediumMedium, + color=MainVariant2, + ) +} + +@Preview(showBackground = true) +@Composable +fun TopTitlePreviewWithBadge() { + CallFromAiTheme { + TopTitle( + title = "어떤 매력에\n끌리나요?", + description = "우선 순위에 따라 최대 5개를 선택해주세요.", + currentStep = 3, + totalStep = 4, + ) + } + +} + +@Preview(showBackground = true) +@Composable +fun TopTitlePreviewWithoutBadge() { + CallFromAiTheme { + TopTitle( + title = "먼저 나를\n소개해볼까요?", + description = "전화왔어요에서 사용할 나의 프로필을 완성해주세요.", + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/res/drawable/onboarding_check.png b/feature/onboarding/impl/src/main/res/drawable/onboarding_check.png new file mode 100644 index 0000000000000000000000000000000000000000..58ad8d5cc58459dcd2a0ec4594e5fa03ab871b68 GIT binary patch literal 16956 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%0A?FQ{O`9G(&eNbuLSVO23Z-hXm$j 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 + + + + + + + From 688fa48b75cca5d99cabfda97517841b47a200ca Mon Sep 17 00:00:00 2001 From: dada4 Date: Sat, 25 Jul 2026 02:33:56 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/call/impl/component/BirthdayChoice.kt | 4 + .../kr/co/call/impl/component/MemberChoice.kt | 207 +++++++++++++++++- .../kr/co/call/impl/component/PreferTime.kt | 49 +++++ .../impl/component/RelatonshipChoiceCard.kt | 140 ++++++++++++ .../kr/co/call/impl/component/SpeechChoice.kt | 87 ++++++++ .../impl/src/main/res/drawable/ic_down.png | Bin 0 -> 262 bytes .../impl/src/main/res/drawable/ic_up.png | Bin 0 -> 247 bytes .../main/res/drawable/relationship_first.png | Bin 0 -> 9489 bytes .../main/res/drawable/relationship_old.png | Bin 0 -> 9752 bytes .../main/res/drawable/relationship_some.png | Bin 0 -> 7382 bytes 10 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/SpeechChoice.kt create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_down.png create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_up.png create mode 100644 feature/onboarding/impl/src/main/res/drawable/relationship_first.png create mode 100644 feature/onboarding/impl/src/main/res/drawable/relationship_old.png create mode 100644 feature/onboarding/impl/src/main/res/drawable/relationship_some.png diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt new file mode 100644 index 00000000..6aa81d87 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.component + +class BirthdayChoice { +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt index 649b13e2..6fcf66d1 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -1,6 +1,211 @@ package kr.co.call.impl.component +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.Gray900 +import kr.co.call.designsystem.theme.MainVariant3 +import kr.co.call.designsystem.theme.White +import kr.co.call.onboarding.impl.R +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.Gray100 +import kr.co.call.designsystem.theme.SubRed @Composable -fun MemberChoice(){} \ No newline at end of file +fun MemberChoice( + modifier: Modifier=Modifier, + label: String, + selectedOption: String, //드롭다운이 보여줄 값 + placeholder: String, + options:List, //선택 가능한 목록 + onOptionSelected: (String)->Unit,//선택됐을 때 실행할 동작 + required: Boolean=false, + ){ + var expanded by rememberSaveable { + mutableStateOf(false) + } + + Column( + modifier=modifier, + ){ + Row{ + Text( + text=label, + color= Gray600, + style= CallTheme.typography.bodyMedium, + ) + if (required){ + Spacer(modifier=Modifier.width(3.dp)) + + Text( + text="*", + color= SubRed, + style= CallTheme.typography.bodyMedium, + ) + } + } + Spacer(modifier=Modifier.height(7.dp)) + + BoxWithConstraints( + modifier=Modifier.fillMaxWidth(), + ){ + val menuWidth=maxWidth + + Surface( + modifier=Modifier + .fillMaxWidth() + .height(55.dp) + .clickable( + role= Role.Button, + onClick={ + expanded= !expanded + }, + ), + color=Gray100, + shape=RoundedCornerShape(10.dp), + ){ + Row( + modifier= Modifier + .fillMaxSize() + .padding(horizontal=17.dp), + verticalAlignment= Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ){ + Text( + text=selectedOption.ifBlank { + placeholder + }, + color=if(selectedOption.isBlank()){Gray600 + } else{Gray900}, + style= CallTheme.typography.bodyMediumMedium + ) + Icon( + painter= painterResource( + id=if(expanded){ + R.drawable.ic_up + }else{ + R.drawable.ic_down + }, + ), + contentDescription=null, + modifier=Modifier.size( + width=12.dp, + height=6.dp, + ), + tint = Color.Unspecified + ) + } + } + + DropdownMenu( + expanded=expanded, + onDismissRequest = { + expanded=false + }, + modifier= Modifier + .width(menuWidth) + .heightIn(max=420.dp), + offset=DpOffset( + x=0.dp, + y=4.dp, + ), + shape= RoundedCornerShape(15.dp), + containerColor = White, + shadowElevation=4.dp, + ) { + options.forEach{ option -> + val isSelected= option == selectedOption + + DropdownMenuItem( + modifier= Modifier + .fillMaxWidth() + .background( + color=if(isSelected){ + MainVariant3 + }else { + White + }, + ), + text={ + Text( + text=option, + color= Black, + style= CallTheme.typography.bodyMediumMedium, + ) + }, + contentPadding= PaddingValues( + horizontal=17.dp, + vertical=9.dp, + ), + onClick={ + onOptionSelected(option) + expanded=false + }, + ) + } + } + } + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF, +) +@Composable +private fun MemberChoicePreview() { + var selectedJob by rememberSaveable { + mutableStateOf("") + } + + MemberChoice( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + label = "직업", + selectedOption = selectedJob, + placeholder = "직업을 선택해주세요", + options = listOf( + "대학생", + "직장인", + "무직", + ), + onOptionSelected = { option -> + selectedJob = option + }, + required = true, + ) +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt new file mode 100644 index 00000000..533e4dab --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt @@ -0,0 +1,49 @@ +package kr.co.call.impl.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +enum class PreferTime( + val emoji: String, + val title: String, + val time: String?, + val description: String, +){ + MORNING( + emoji = "🌅", + title = "오전 시간대", + time = "08:00 ~ 12:00", + description = "하루를 기분 좋게 시작할 수 있도록 아침 인사를 전해드려요.", + ), + + DAY( + emoji = "🏙️", + title = "낮 시간대", + time = "13:00 ~ 18:00", + description = "바쁜 일상 속 잠시 쉬어갈 수 있도록 가볍게 안부를 물어봐요.", + ), + + LATE_EVENING( + emoji = "🌃", + title = "늦은 오후 시간대", + time = "19:00 ~ 24:00", + description = "하루를 마무리하며 더 깊은 대화와 통화를 나눠요.", + ), + + ANYTIME( + emoji = "☺️", + title = "언제든 좋아요", + time = null, + description = "시간 제한 없이 언제든 전화할게요.", + ), +} + +@Composable +fun PreferTimeItem( + preferTime: PreferTime, + selected: Boolean, + onClick:()->Unit, + modifier: Modifier =Modifier, +){ + +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt new file mode 100644 index 00000000..a6b8dd42 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt @@ -0,0 +1,140 @@ +package kr.co.call.impl.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.ChatGradient +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.MainVariant3 +import kr.co.call.designsystem.theme.MainVariant5Chat +import kr.co.call.designsystem.theme.White +import kr.co.call.onboarding.impl.R + +@Composable +fun RelationshipChoiceCard( + @DrawableRes imageRes: Int, + title: String, + description: String, + selected: Boolean, + onClick:() -> Unit, + modifier: Modifier =Modifier, +){ + Surface( + modifier = modifier + .size( + width = 116.dp, + height = 185.dp, + ) + .selectable( + selected=selected, + onClick=onClick, + role=Role.RadioButton, + ), + shape= RoundedCornerShape(20.dp), + color=if (selected){ + MainVariant5Chat + } else{White}, + border=if(selected){ + BorderStroke( + width=1.5.dp, + color= MainVariant3, + ) + }else {null}, + ){ + Column( + modifier= Modifier + .fillMaxSize() + .padding( + horizontal=12.dp, + vertical=26.dp, + ), + horizontalAlignment=Alignment.CenterHorizontally, + ){ + Image( + painter= painterResource(id=imageRes), + contentDescription=title, + modifier=Modifier + .fillMaxWidth() + .height(75.dp), + contentScale = ContentScale.Fit, + ) + + Spacer(modifier=Modifier.height(10.dp)) + + Text( + text=title, + style= CallTheme.typography.bodyLargeBold.copy( + brush=ChatGradient + ), + textAlign =TextAlign.Center, + maxLines=1, + ) + + Spacer(modifier=Modifier.height(5.dp)) + + Text( + text=description, + style= CallTheme.typography.captionBold, + color= Gray400, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } + } +} + +@Preview( + name = "Relationship Card States", + showBackground = true, + widthDp = 280, +) +@Composable +private fun RelationshipChoiceCardPreview() { + CallFromAiTheme { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + RelationshipChoiceCard( + imageRes = R.drawable.relationship_old, + title = "오래된 연인", + description = "편안한 관계", + selected = false, + onClick = {}, + ) + + RelationshipChoiceCard( + imageRes = R.drawable.relationship_old, + title = "오래된 연인", + description = "편안한 관계", + selected = true, + onClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/SpeechChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/SpeechChoice.kt new file mode 100644 index 00000000..26ab9bf1 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/SpeechChoice.kt @@ -0,0 +1,87 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.MainVariant1 +import kr.co.call.designsystem.theme.MainVariant5Chat +import kr.co.call.designsystem.theme.White + +@Composable +fun SpeechChoice( + text: String, + selected: Boolean, + onClick: ()->Unit, + modifier: Modifier =Modifier, +){ + Surface( + modifier=modifier.selectable( + selected=selected, + onClick=onClick, + role=Role.RadioButton, + ), + shape= RoundedCornerShape(10.dp), + color= if(selected){ + MainVariant5Chat + }else {White}, + border=if(selected){ + BorderStroke( + width=1.dp, + color=MainVariant1, + ) + }else{null}, + ){ + Text( + text=text, + modifier=Modifier.padding( + horizontal=20.dp, + vertical=10.dp + ), + style= CallTheme.typography.bodyMediumMedium, + color= MainVariant1, + textAlign= TextAlign.Center, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SpeechChoicePreview() { + CallFromAiTheme { + Row( + modifier = Modifier.padding(20.dp), + horizontalArrangement = Arrangement.spacedBy(17.dp), + ) { + SpeechChoice( + text = "반말", + selected = false, + onClick = {}, + ) + + SpeechChoice( + text = "반존대", + selected = true, + onClick = {}, + ) + + SpeechChoice( + text = "존댓말", + selected = false, + onClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/res/drawable/ic_down.png b/feature/onboarding/impl/src/main/res/drawable/ic_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2ceac5b9e1a0e93464aaf2a6fafbcd1e27975e5e GIT binary patch literal 262 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 IM6N<$f;!V3x&QzG literal 0 HcmV?d00001 From 9d46a6def47634b4f0e62d7cdae9b482a54f7692 Mon Sep 17 00:00:00 2001 From: dada4 Date: Sun, 26 Jul 2026 02:47:00 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/call/designsystem/theme/Color.kt | 10 + .../kr/co/call/designsystem/theme/Type.kt | 2 + .../kr/co/call/impl/component/BottomWheel.kt | 3 + .../co/call/impl/component/KeywordChoice.kt | 184 +++++++++++ .../call/impl/component/MessageInputField.kt | 191 +++++++++++ .../kr/co/call/impl/component/PreferTime.kt | 164 +++++++++- .../co/call/impl/component/ProfileChoice.kt | 23 +- .../kr/co/call/impl/component/TempControl.kt | 303 ++++++++++++++++++ .../kr/co/call/impl/component/TopTitle.kt | 142 +++++--- .../co/call/impl/screen/Onboarding1Screen.kt | 4 + .../co/call/impl/screen/Onboarding2Screen.kt | 4 + .../co/call/impl/screen/Onboarding3Screen.kt | 4 + .../co/call/impl/screen/Onboarding4Screen.kt | 4 + .../impl/src/main/res/drawable/ic_left.xml | 13 + .../impl/src/main/res/drawable/ic_right.xml | 13 + .../main/res/drawable/onboarding_profile2.xml | 30 ++ .../src/main/res/drawable/spendbutton.png | Bin 0 -> 870 bytes 17 files changed, 1052 insertions(+), 42 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_left.xml create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_right.xml create mode 100644 feature/onboarding/impl/src/main/res/drawable/onboarding_profile2.xml create mode 100644 feature/onboarding/impl/src/main/res/drawable/spendbutton.png diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt index a743e514..77d82318 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Color.kt @@ -23,6 +23,9 @@ val SubGray2 = Color(0xFFF7F3F3) val SubRed = Color(0xFFFF0000) val SubBlue = Color(0xFF4391FF) +val SubFire=Color(0xFFFF5677) +val SubSoup=Color(0xFFFFCE91) + val SubPressed = Color(0xFFED5988) val SubPressed2 = Color(0xFFFCB4C1) val SubPressed3 = Color(0xFF1F0A03) @@ -52,6 +55,13 @@ val ChatGradient = Brush.horizontalGradient( ) ) +val DragGradient=Brush.horizontalGradient( + listOf( + Color(0xFFFFD092), + Color(0xFFFF4F75) + ) +) + // ---------- Design System Colors ---------- @Immutable data class CallColors( diff --git a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt index a185fa50..c3ae07a0 100644 --- a/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt +++ b/core/designsystem/src/main/java/kr/co/call/designsystem/theme/Type.kt @@ -54,6 +54,8 @@ data class CallTypography( // Others (line height 150%) val caption: TextStyle = pretendard(12, 1.5f, FontWeight.Normal), val captionBold: TextStyle = pretendard(12, 1.5f, FontWeight.SemiBold), + val captionSmall: TextStyle = pretendard(8,1.5f, FontWeight.Normal), + val miniMedium: TextStyle = pretendard(11,1.5f,FontWeight.Normal), ) val LocalCallTypography = staticCompositionLocalOf { CallTypography() } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt new file mode 100644 index 00000000..9b10adfa --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt @@ -0,0 +1,3 @@ +package kr.co.call.impl.component + +fun BottomWheel(){} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt new file mode 100644 index 00000000..ed4d3ae6 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt @@ -0,0 +1,184 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray200 +import kr.co.call.designsystem.theme.MainVariant1 +import kr.co.call.designsystem.theme.SubGray +import kr.co.call.designsystem.theme.White + +enum class Trait( + val keyword: String, + val emoji: String, + val label: String, +){ + HUMOROUS("HUMOROUS", "😃", "유머러스한"), + PLAYFUL("PLAYFUL", "🤡", "장난기 많은"), + AFFECTIONATE("AFFECTIONATE", "💕", "애교 많은"), + JEALOUS("JEALOUS", "❤️‍🔥", "질투심 폭발"), + TALKATIVE("TALKATIVE", "🗣️", "수다쟁이"), + DAD_JOKE_LOVER("DAD_JOKE_LOVER", "😝", "아재개그 좋아하는"), + HOMEBODY("HOMEBODY", "🛌", "집순이/집돌이"), + TEASING("TEASING", "🏀", "놀리는 걸 좋아하는"), + POSSESSIVE("POSSESSIVE", "📱", "집착하는"), + TSUNDERE("TSUNDERE", "👋", "츤데레"), + EXPRESSIVE("EXPRESSIVE", "😌", "표현을 많이 하는"), + PET_NAME_LOVER("PET_NAME_LOVER", "👄", "애칭을 자주 쓰는"), + EXCLUSIVE("EXCLUSIVE", "🪴", "독점욕이 있는"), + QUIRKY("QUIRKY", "👍", "4차원 같은"), + LAID_BACK("LAID_BACK", "🕶️", "털털한"), + OPENLY_JEALOUS("OPENLY_JEALOUS", "📑", "질투를 숨기지 않는"), + SHY("SHY", "🧐", "부끄러움을 많이 타는"), + SMOOTH_TALKER("SMOOTH_TALKER", "☄\uFE0F", "능청스러운"), + FREQUENT_CHECKER("FREQUENT_CHECKER", "💌", "연락을 자주 확인하는"), + GOOD_LISTENER("GOOD_LISTENER", "👐", "고민을 잘 들어주는"), + COMPLIMENTER("COMPLIMENTER", "😍", "칭찬을 많이 하는"); + + companion object { + fun fromKeyword(key: String): Trait? { + return entries.find { it.keyword == key } + } + } +} + +@Composable +fun KeywordChoice( + emoji: String, + text: String, + selectedOrder: Int?, + onClick:()->Unit, + modifier: Modifier =Modifier, +){ + Box( + modifier=modifier, + ){ + Surface( + modifier=Modifier.toggleable( + value = selectedOrder !=null, + onValueChange={ + onClick() + }, + role= Role.Checkbox, + ), + shape= RoundedCornerShape(30.dp), + color= if (selectedOrder != null){ + MainVariant1 + } else { White}, + border=if (selectedOrder != null){ + null + }else { + BorderStroke( + width = 1.dp, + color = Gray200 + ) + } + ){ + Row( + modifier=Modifier.padding( + horizontal=15.dp, + vertical=5.dp, + ), + verticalAlignment=Alignment.CenterVertically, + ){ + Text( + text=emoji, + style= CallTheme.typography.bodyMediumMedium + ) + Spacer(modifier=Modifier.width(7.dp)) + Text( + text=text, + color=if (selectedOrder !=null){ + White + }else { + Black}, + style= CallTheme.typography.bodyMediumMedium + ) + } + } + if (selectedOrder != null){ + Surface( + modifier= Modifier + .align(Alignment.TopStart) + .offset( + x=(-5).dp, + y=(-5).dp, + ) + .size(20.dp), + shape= CircleShape, + color=White, + border= BorderStroke( + width=1.5.dp, + color= MainVariant1, + ), + ) { + Box( + contentAlignment=Alignment.Center + ){ + Text( + text=selectedOrder.toString(), + color=MainVariant1, + style= CallTheme.typography.miniMedium, + ) + } + } + } + } +} + +@Preview( + name = "KeywordChoice 선택 전", + showBackground = true, + backgroundColor = 0xFFE9E5E7 +) +@Composable +private fun KeywordChoiceUnselectedPreview() { + CallFromAiTheme { + val trait = Trait.JEALOUS // Trait 객체 직접 지정 + KeywordChoice( + emoji = trait.emoji, + text = trait.label, + selectedOrder = null, + onClick = {}, + modifier = Modifier.padding(20.dp), + ) + } +} + +@Preview( + name = "KeywordChoice 선택 후", + showBackground = true, + backgroundColor = 0xFFE9E5E7 +) +@Composable +private fun KeywordChoiceSelectedPreview() { + CallFromAiTheme { + val trait = Trait.HUMOROUS + KeywordChoice( + emoji = trait.emoji, + text = trait.label, + selectedOrder = 1, + onClick = {}, + modifier = Modifier.padding(20.dp), + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt new file mode 100644 index 00000000..e298c719 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt @@ -0,0 +1,191 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width +import androidx.compose.foundation.shape.CircleShape +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.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +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 kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.Gray900 +import kr.co.call.designsystem.theme.MainVariant1 +import kr.co.call.designsystem.theme.SubGray +import kr.co.call.onboarding.impl.R +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import kr.co.call.designsystem.theme.CallFromAiTheme +import androidx.compose.runtime.remember +import kr.co.call.designsystem.theme.Gray600 + +@Composable +fun MessageInputField( + value: String, + onValueChange: (String) -> Unit, + onSendClick: () -> Unit, + modifier: Modifier = Modifier, + placeholder: String = "", +){ + val maxLength=10 + val focusManager= LocalFocusManager.current + val canSend=value.isNotBlank() && value.length <= maxLength + + Row( + modifier=modifier + .fillMaxWidth() + .height(56.dp) + .background( + color=SubGray, + shape= RoundedCornerShape(50.dp) + ) + .padding( + start=23.dp, + top=4.dp, + end=4.dp, + bottom=4.dp + ), + verticalAlignment = Alignment.CenterVertically, + ){ + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.weight(1f), + singleLine = true, + textStyle = CallTheme.typography.bodySmallBold.copy( + color = Gray900, + ), + cursorBrush = SolidColor(MainVariant1), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Send, + ), + keyboardActions = KeyboardActions( + onSend = { + if (canSend) { + onSendClick() + focusManager.clearFocus() + } + }, + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + if (value.isEmpty() && placeholder.isNotBlank()) { + Text( + text = placeholder, + color = Gray900, + style = CallTheme.typography.bodySmallBold, + ) + } + + innerTextField() + } + }, + ) + Text( + text = "${value.length}/$maxLength", + color = if (value.isEmpty() || value.length>maxLength) { + Gray600 + } else { + MainVariant1 + }, + style = CallTheme.typography.bodySmallBold, + ) + Spacer(modifier = Modifier.width(5.dp)) + + Image( + painter = painterResource(R.drawable.spendbutton), + contentDescription = "전송", + contentScale = ContentScale.Fit, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .clickable( + enabled = canSend, + role = Role.Button, + onClick = onSendClick, + ), + ) + } +} + +@Preview( + name = "10글자 미만", + showBackground = true, + widthDp = 390, +) +@Composable +private fun MessageInputFieldUnderLimitPreview() { + var message by remember { + mutableStateOf("안녕하세요") // 5글자 + } + + CallFromAiTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) { + MessageInputField( + value = message, + onValueChange = { message = it }, + onSendClick = { message = "" }, + placeholder = "메시지를 입력해주세요.", + ) + } + } +} + +@Preview( + name = "10글자 초과", + showBackground = true, + widthDp = 390, +) +@Composable +private fun MessageInputFieldOverLimitPreview() { + var message by remember { + mutableStateOf("안녕하세요반갑습니다!") // 11글자 + } + + CallFromAiTheme { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) { + MessageInputField( + value = message, + onValueChange = { message = it }, + onSendClick = { message = "" }, + placeholder = "메시지를 입력해주세요.", + ) + } + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt index 533e4dab..95c86db2 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt @@ -1,7 +1,41 @@ package kr.co.call.impl.component +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxSize +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.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.MainVariant1 +import kr.co.call.designsystem.theme.MainVariant2 +import kr.co.call.designsystem.theme.White +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue enum class PreferTime( val emoji: String, @@ -39,11 +73,137 @@ enum class PreferTime( } @Composable -fun PreferTimeItem( +fun PreferTime( preferTime: PreferTime, selected: Boolean, onClick:()->Unit, modifier: Modifier =Modifier, -){ +) { + Surface( + modifier = modifier + .fillMaxWidth() + .height(93.dp) + .selectable( + selected = selected, + onClick = onClick, + role = Role.RadioButton, + ), + shape = RoundedCornerShape(20.dp), + color = if (selected) { + MainVariant2 + } else { + White + }, + border = if (selected) { + BorderStroke( + width = 1.dp, + color = MainVariant1, + ) + } else { + null + }, + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + start = 17.dp, + end = 16.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier= Modifier + .size(20.dp) + .border( + width=3.dp, + color=if(selected){ + White + }else {MainVariant2}, + shape= CircleShape, + ) + .background( + color= if(selected){ + MainVariant1 + }else {White}, + shape=CircleShape, + ), + ){ + } + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = preferTime.emoji, + style = CallTheme.typography.titleSmallBold + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = preferTime.title, + style = CallTheme.typography.bodyMediumMedium, + color = Black, + maxLines = 1, + ) + + preferTime.time?.let { time -> + Spacer(modifier = Modifier.width(13.dp)) + Text( + text = time, + style = CallTheme.typography.captionBold, + color = MainVariant1, + maxLines = 1, + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = preferTime.description, + style = CallTheme.typography.caption, + color = Gray600, + maxLines = 1, + ) + } + } + } +} + +@Preview( + showBackground = true, + widthDp = 420, + heightDp = 500, +) +@Composable +private fun PreferTimePreview() { + var selectedPreferTime by rememberSaveable { + mutableStateOf(PreferTime.DAY) + } + CallFromAiTheme { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + PreferTime.entries.forEach { preferTime -> + PreferTime( + preferTime = preferTime, + selected = selectedPreferTime == preferTime, + onClick = { + selectedPreferTime = preferTime + }, + ) + } + } + } + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt index 63c9fcca..c326853d 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt @@ -2,12 +2,15 @@ package kr.co.call.impl.component import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector @@ -19,6 +22,8 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.onboarding.impl.R +import androidx.compose.runtime.getValue + @Composable fun ProfileChoice( @@ -27,14 +32,24 @@ fun ProfileChoice( onClick: ()->Unit, size: Dp =99.dp ){ + val interactionSource=remember{MutableInteractionSource()} + val isPressed by interactionSource.collectIsPressedAsState() + Box( modifier=modifier .size(size) - .clickable(onClick=onClick) + .clickable( + interactionSource=interactionSource, + indication = null, + onClick=onClick, ) ) { if (imageUrl.isNullOrEmpty()){ Image( - imageVector= ImageVector.vectorResource(id= R.drawable.onboarding_profile), + imageVector= ImageVector.vectorResource( + id= if(isPressed){ + R.drawable.onboarding_profile2 + }else {R.drawable.onboarding_profile}, + ), contentDescription="기본 프로필 이미지", modifier=Modifier.fillMaxSize(), contentScale= ContentScale.Fit @@ -54,7 +69,7 @@ fun ProfileChoice( @Preview( name = "기본 프로필 이미지", - showBackground = true + showBackground = true, ) @Composable private fun ProfileChoicePreview() { @@ -62,7 +77,7 @@ private fun ProfileChoicePreview() { ProfileChoice( modifier = Modifier.padding(20.dp), imageUrl = null, - onClick = {} + onClick = {}, ) } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt new file mode 100644 index 00000000..7dbb275a --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt @@ -0,0 +1,303 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.MainVariant1 +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.DragGradient +import kr.co.call.designsystem.theme.SubFire +import kr.co.call.designsystem.theme.SubSoup +import kr.co.call.designsystem.theme.White +import kr.co.call.onboarding.impl.R +import kotlin.math.roundToInt + + +@Composable +fun TempControl ( + modifier: Modifier= Modifier, + onTemperatureChange: (Int)->Unit, +) { + var temperature by rememberSaveable { + mutableIntStateOf(50) + } + Column( + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "연애 온도 : ", + color = Black, + style = CallTheme.typography.bodyMediumMedium, + ) + Text( + text = "$temperature ºC", + color = MainVariant1, + style = CallTheme.typography.bodyMediumMedium, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "드래그해서 나에게 맞는 스타일을 선택해보세요!", + color = Gray400, + style = CallTheme.typography.caption, + ) + Spacer(modifier = Modifier.height(15.dp)) + TempDrag( + temperature=temperature, + onTemperatureChange={ + temperature=it + onTemperatureChange(it) + }, + ) + } + } + +@Composable +private fun TempDrag( + temperature: Int, + onTemperatureChange: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .height(151.dp), + ) { + val density=LocalDensity.current + // 버튼 중심이 왼쪽 카드의 오른쪽 끝에 위치하는 지점 + val minimumOffsetPx=with(density){ + (98.dp-24.dp).toPx() + } + // 버튼 중심이 오른쪽 카드의 왼쪽 끝에 위치하는 지점 + val maximumOffsetPx=constraints.maxWidth.toFloat()-with(density){ + (98.dp+24.dp).toPx() + } + val dragDistancePx=maximumOffsetPx-minimumOffsetPx + var thumbOffsetPx by remember( + minimumOffsetPx, + maximumOffsetPx, + ){ + mutableFloatStateOf( + minimumOffsetPx+dragDistancePx*(temperature/100f), + ) + } + val draggableState=rememberDraggableState{dragAmount -> + thumbOffsetPx=( + thumbOffsetPx+dragAmount + ).coerceIn( + minimumValue=minimumOffsetPx, + maximumValue=maximumOffsetPx, + ) + val progress=( + (thumbOffsetPx-minimumOffsetPx)/ + dragDistancePx + ).coerceIn(0f,1f) + val changedTemperature= + (progress*10f).roundToInt()*10 + if (changedTemperature != temperature){ + onTemperatureChange(changedTemperature) + } + } + Box( + modifier= Modifier + .align(Alignment.Center) + .padding(horizontal = 98.dp) + .fillMaxWidth() + .height(13.dp) + .background(brush= DragGradient), + ) + TempCard( + isHot=false, + modifier= Modifier.align( + Alignment.CenterStart, + ), + ) + + TempCard( + isHot=true, + modifier=Modifier.align( + Alignment.CenterEnd, + ), + ) + Surface( + modifier= Modifier + .align(Alignment.CenterStart) + .offset { + IntOffset( + x=thumbOffsetPx.roundToInt(), + y=0, + ) + } + .size(47.dp) + .draggable( + state=draggableState, + orientation=Orientation.Horizontal, + onDragStopped={ + val progress=( + (thumbOffsetPx-minimumOffsetPx)/dragDistancePx + ).coerceIn( + minimumValue = 0f, + maximumValue=1f, + ) + val snappedTemperature=(progress*10f).roundToInt()*10 + thumbOffsetPx=minimumOffsetPx+dragDistancePx*(snappedTemperature/100f) + onTemperatureChange(snappedTemperature,) + }, + ), + shape=CircleShape, + color=White, + shadowElevation = 10.dp, + ){ + Row( + modifier=Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ){ + Icon( + painter= painterResource( + id=R.drawable.ic_left, + ), + contentDescription = null, + tint=Color.Unspecified, + modifier=Modifier.size(14.dp), + ) + Spacer(modifier=Modifier.width(10.dp)) + Icon( + painter= painterResource( + id=R.drawable.ic_right, + ), + contentDescription = null, + tint=Color.Unspecified, + modifier=Modifier.size(14.dp), + ) + } + } + } +} + +@Composable +private fun TempCard( + isHot: Boolean, //어떤 카드인지 구분하는 용(T: 불닭맛) + modifier: Modifier=Modifier, +){ + Surface( + modifier=modifier + .width(98.dp) + .height(151.dp), + shape=RoundedCornerShape(20.dp), + color=White, + ){ + Column( + modifier= Modifier + .fillMaxSize() + .padding( + horizontal=12.dp, + vertical=9.dp, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ){ + Box( + modifier= Modifier + .size(40.dp) + .background( + color=if(isHot){ + SubFire + }else { + SubSoup}, + shape= CircleShape, + ), + contentAlignment = Alignment.Center, + ){ + Text( + text= if(isHot) "\uD83C\uDF36\uFE0F" else "\uD83C\uDF72", + style= CallTheme.typography.titleMediumBold, + textAlign = TextAlign.Center, + ) + } + Spacer( + modifier=Modifier.height(11.dp), + ) + Text( + text=if(isHot) {"도파민 뿜뿜"} else {"파워 안정형"}, + color=if(isHot){SubFire} else {SubSoup}, + style= CallTheme.typography.caption, + textAlign=TextAlign.Center, + maxLines=1, + ) + Text( + text=if(isHot){"불닭 맛"} else {"능이백숙 맛"}, + color=Black, + style= CallTheme.typography.bodySmallBold, + textAlign = TextAlign.Center, + maxLines=1, + ) + Spacer(modifier=Modifier.height(14.dp)) + Text( + text=if(isHot){"과감하고 표현이 많은 편\n화끈하고 짜릿한 스타일"} + else {"다정하고 배려가 많은 편\n천천히 오래가는 스타일"}, + color=Gray400, + style= CallTheme.typography.captionSmall + ) + } + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFF8F4F5, +) +@Composable +private fun TempControlPreview() { + CallFromAiTheme { + TempControl( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 24.dp, + ), + onTemperatureChange = {}, + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt index 64f839d0..27bc8e7d 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt @@ -1,22 +1,31 @@ package kr.co.call.impl.component +import androidx.compose.foundation.background +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 import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.Black import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray100 import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.MainGradient import kr.co.call.designsystem.theme.MainVariant2 +import kr.co.call.designsystem.theme.MainVariant3 @Composable fun TopTitle( @@ -25,51 +34,108 @@ fun TopTitle( description: String, currentStep: Int?=null, totalStep: Int?=null, - ){ + ) { + val progress = if ( + currentStep != null && totalStep != null && totalStep > 0 + ) { + (currentStep.toFloat() / totalStep.toFloat()).coerceIn(0f, 1f) + } else { + null + } + Column( modifier = modifier.fillMaxWidth(), - ){ - Row( - modifier= Modifier - .fillMaxWidth() - .padding(end=14.dp), - verticalAlignment=Alignment.CenterVertically, - ){ - Text( - text=title, - style=CallTheme.typography.titleMediumBold, - color=Black, - modifier=Modifier.weight(1f, fill=true), + ) { + if (progress != null) { + Box( + modifier= Modifier + .fillMaxWidth() + .height(4.dp) + .background(Gray100) + ){ + Box( + modifier= Modifier + .fillMaxWidth(progress) + .fillMaxHeight() + .clip( + RoundedCornerShape( + topEnd = 100.dp, + bottomEnd = 100.dp), + ) + .background(brush= MainGradient), + ) + } + Spacer( + modifier = Modifier.height(23.dp), ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 14.dp), + verticalAlignment = Alignment.Top, + ) { + Text( + text = title, + style = CallTheme.typography.titleMediumBold, + color = Black, + modifier = Modifier.weight(1f), + ) - if (currentStep != null && totalStep != null){ - StepNumber(currentStep=currentStep, totalStep = totalStep) + if (currentStep != null && totalStep != null) { + Spacer(modifier= Modifier.padding(4.dp)) + Text( + text="$currentStep/$totalStep", + style= CallTheme.typography.bodyMediumMedium, + color= MainVariant3, + ) + } } + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = description, + style = CallTheme.typography.bodySmall, + color = Gray400, + ) } - Spacer(modifier=Modifier.height(3.dp)) - Text( - text=description, - style= CallTheme.typography.bodySmall, - color= Gray400, + } + +@Preview( + name = "1단계 - 진행바 없음", + showBackground = true, +) +@Composable +private fun TopTitleStep1Preview() { + CallFromAiTheme { + TopTitle( + title = "먼저 나를\n소개해볼까요?", + description = "전화왔어에서 사용할 나의 프로필을 완성해주세요.", ) } } +@Preview( + name = "2단계", + showBackground = true, +) @Composable -fun StepNumber( - currentStep: Int, - totalStep: Int, -){ - Text( - text="$currentStep/$totalStep", - style= CallTheme.typography.bodyMediumMedium, - color=MainVariant2, - ) +private fun TopTitleStep2Preview() { + CallFromAiTheme { + TopTitle( + title = "어떤 사이로\n시작할까요?", + description = "원하는 상대방과의 관계를 선택해주세요.", + currentStep = 2, + totalStep = 4, + ) + } } -@Preview(showBackground = true) +@Preview( + name = "3단계", + showBackground = true, +) @Composable -fun TopTitlePreviewWithBadge() { +private fun TopTitleStep3Preview() { CallFromAiTheme { TopTitle( title = "어떤 매력에\n끌리나요?", @@ -78,16 +144,20 @@ fun TopTitlePreviewWithBadge() { totalStep = 4, ) } - } -@Preview(showBackground = true) +@Preview( + name = "4단계", + showBackground = true, +) @Composable -fun TopTitlePreviewWithoutBadge() { +private fun TopTitleStep4Preview() { CallFromAiTheme { TopTitle( - title = "먼저 나를\n소개해볼까요?", - description = "전화왔어요에서 사용할 나의 프로필을 완성해주세요.", + title = "언제 먼저 통화를\n걸어드리면 좋을까요?", + description = "편하게 통화하기 좋은 시간대를 선택해주세요.", + currentStep = 4, + totalStep = 4, ) } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt new file mode 100644 index 00000000..7d2db8fa --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.screen + +class Onboarding1Screen { +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt new file mode 100644 index 00000000..d07b356f --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.screen + +class Onboarding2Screen { +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt new file mode 100644 index 00000000..adb6d1d5 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.screen + +class Onboarding3Screen { +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt new file mode 100644 index 00000000..3f72750c --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -0,0 +1,4 @@ +package kr.co.call.impl.screen + +class Onboarding4Screen { +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/res/drawable/ic_left.xml b/feature/onboarding/impl/src/main/res/drawable/ic_left.xml new file mode 100644 index 00000000..d76ff0f0 --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/ic_left.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/onboarding/impl/src/main/res/drawable/ic_right.xml b/feature/onboarding/impl/src/main/res/drawable/ic_right.xml new file mode 100644 index 00000000..9afef04a --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/ic_right.xml @@ -0,0 +1,13 @@ + + + diff --git a/feature/onboarding/impl/src/main/res/drawable/onboarding_profile2.xml b/feature/onboarding/impl/src/main/res/drawable/onboarding_profile2.xml new file mode 100644 index 00000000..223d1b30 --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/onboarding_profile2.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/feature/onboarding/impl/src/main/res/drawable/spendbutton.png b/feature/onboarding/impl/src/main/res/drawable/spendbutton.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f7e7ad6d072482b5fbba7934e3bf077a323af4 GIT binary patch literal 870 zcmV-s1DX7ZP)odB~)<`5?#W7 zkQ5L?gWV7X3X_%wr$a*EP$5xWK@EurHBRUN2|Gdw;KwktXIp-I_s%x{Nhja!?e2Zv zo!Qx0f+L*FPT^EJ4))^k=#msoSFaj5b5(c;RPk{X^C_9g!vV6b$ZId-pxB%US&+vm zxRF5_DOyeBY%2E^#cSDV#E_QTIYh@woY(6muV~rY98a^4im$=hf*fxFqsSh_(=g#&zo;T3@eDt9=)Ce)4f7_g%rCYUcRb)Q6^A{Q72fD12F w&OC7$=Df3UOKtXNa^{I`OpuRXa6a?=2Lrwl<+bB#V*mgE07*qoM6N<$f(Pn{+5i9m literal 0 HcmV?d00001 From 3e766dd2461cbef427643173919ed28f216020f9 Mon Sep 17 00:00:00 2001 From: dada4 Date: Mon, 27 Jul 2026 01:48:51 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 15 +- .../java/kr/co/call/api/OnboardingRoute.kt | 13 +- .../co/call/impl/component/BirthdayChoice.kt | 131 ++++++++- .../kr/co/call/impl/component/BottomWheel.kt | 25 ++ .../kr/co/call/impl/component/MemberChoice.kt | 6 +- .../call/impl/component/MessageInputField.kt | 19 +- .../java/kr/co/call/impl/component/NameBox.kt | 41 +-- .../kr/co/call/impl/component/TopTitle.kt | 2 +- .../call/impl/entry/OnboardingEntryBuilder.kt | 28 +- .../co/call/impl/screen/Onboarding1Screen.kt | 266 +++++++++++++++++- .../co/call/impl/screen/Onboarding2Screen.kt | 2 +- .../co/call/impl/screen/Onboarding5Screen.kt | 3 + .../co/call/impl/screen/OnboardingScreen.kt | 20 -- .../src/main/res/drawable/ic_calendar.xml | 13 + 14 files changed, 526 insertions(+), 58 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt delete mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_calendar.xml diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 607f7462..cdfb2fce 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -15,7 +14,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity @@ -29,6 +27,8 @@ import kr.co.call.api.FaqNavKey import kr.co.call.api.HomeNavKey import kr.co.call.api.ManagerChatRoomNayKey import kr.co.call.api.MyPageNavKey +import kr.co.call.api.Onboarding1NavKey +import kr.co.call.api.Onboarding2NavKey import kr.co.call.api.TermNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab @@ -51,7 +51,7 @@ import kr.co.call.callfromai.util.toMainTab @Composable fun AppScreen(modifier: Modifier = Modifier) { // TODO: 로그인 구현 후 로그인 여부에 따른 분기처리 필요. 일단은 시작점을 홈 화면으로 설정 - val backStack = rememberNavBackStack(HomeNavKey) + val backStack = rememberNavBackStack(Onboarding1NavKey) val appNavigator = remember(backStack) { AppNavigator(backStack) } val currentKey = backStack.lastOrNull() @@ -106,7 +106,14 @@ fun AppScreen(modifier: Modifier = Modifier) { modifier = Modifier.fillMaxSize().padding(padding), entryProvider = entryProvider { loginEntry() - onboardingEntry() + onboardingEntry( + onProfileClick = { + // 사진 선택 기능은 나중에 구현 + }, + onNextClick = { _, _, _, _, _ -> + appNavigator.navigate(Onboarding2NavKey) + }, + ) homeEntry() chattingEntry( navigateToChatRoom = { roomId -> diff --git a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt index ad52245a..53f2174d 100644 --- a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt +++ b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt @@ -4,4 +4,15 @@ import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable @Serializable -data object OnboardingNavKey : NavKey +data object Onboarding1NavKey : NavKey + +@Serializable +data object Onboarding2NavKey: NavKey { +} + +@Serializable +data object Onboarding3NavKey: NavKey + +@Serializable +data object Onboarding4NavKey + diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt index 6aa81d87..51183ed9 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -1,4 +1,133 @@ package kr.co.call.impl.component -class BirthdayChoice { +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray100 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.Gray900 +import kr.co.call.designsystem.theme.SubRed +import kr.co.call.onboarding.impl.R +import java.time.LocalDate +import androidx.compose.runtime.setValue +import androidx.compose.runtime.getValue + +@Composable +fun BirthdayChoice( + selectedDate: LocalDate, + onDateSelected: (LocalDate)->Unit, + modifier: Modifier =Modifier, +){ + var showBottomWheel by rememberSaveable { + mutableStateOf(false) + } + Column(modifier=modifier){ + Row( + verticalAlignment=Alignment.CenterVertically + ){ + Text( + text="생년월일 ", + color= Gray600, + style= CallTheme.typography.bodyMedium + ) + Text( + text="*", + color= SubRed, + style= CallTheme.typography.bodyMedium + ) + } + Spacer(modifier=Modifier.height(7.dp)) + + Surface( + modifier= Modifier + .fillMaxWidth() + .height(55.dp) + .clickable( + role=Role.Button, + onClick = { showBottomWheel = true }, + ), + color=Gray100, + shape=RoundedCornerShape(10.dp), + ){ + Row( + modifier= Modifier + .fillMaxSize() + .padding(horizontal=16.dp), + verticalAlignment=Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ){ + Text( + text=selectedDate.toDisplayText(), + color= Gray900, + style = CallTheme.typography.bodyMediumMedium, + ) + + Icon( + painter = painterResource(R.drawable.ic_calendar), + contentDescription = "생년월일 선택", + modifier = Modifier.size( + height=20.dp, + width=18.dp,), + tint = Color.Unspecified, + ) + } + } + } + + if (showBottomWheel) { + BottomWheel( + selectedDate = selectedDate, + onConfirmClick = { newDate -> + onDateSelected(newDate) + showBottomWheel = false + }, + onDismissRequest = { + showBottomWheel = false + }, + ) + } +} + +private fun LocalDate.toDisplayText(): String { + return "$year / $monthValue / $dayOfMonth" +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF, +) +@Composable +private fun BirthdayChoicePreview() { + CallFromAiTheme { + BirthdayChoice( + selectedDate = LocalDate.of(2001, 5, 18), + onDateSelected = {}, + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt index 690c48cf..dc00ba4d 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -16,6 +17,12 @@ import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallTheme import java.time.LocalDate import java.time.YearMonth +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.component.bottomsheet.ConfirmBottomSheet +import kr.co.call.designsystem.component.picker.WheelPicker +import kr.co.call.designsystem.theme.CallFromAiTheme @Composable fun BottomWheel( @@ -135,4 +142,22 @@ fun BottomWheel( } } } +} + + +@Preview( + name = "생년월일 선택", + showBackground = true, + widthDp = 412, + heightDp = 800, +) +@Composable +private fun BottomWheelPreview() { + CallFromAiTheme { + BottomWheel( + selectedDate = LocalDate.of(2001, 5, 18), + onConfirmClick = {}, + onDismissRequest = {}, + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt index 6fcf66d1..ea53ac70 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -87,7 +87,7 @@ fun MemberChoice( Surface( modifier=Modifier .fillMaxWidth() - .height(55.dp) + .height(39.dp) .clickable( role= Role.Button, onClick={ @@ -136,10 +136,10 @@ fun MemberChoice( expanded=false }, modifier= Modifier - .width(menuWidth) + .width(114.dp) .heightIn(max=420.dp), offset=DpOffset( - x=0.dp, + x=menuWidth-114.dp, y=4.dp, ), shape= RoundedCornerShape(15.dp), diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt index e298c719..9744d889 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt @@ -18,6 +18,7 @@ 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.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,6 +42,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import kr.co.call.designsystem.theme.CallFromAiTheme import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import kr.co.call.designsystem.theme.Gray600 @Composable @@ -50,10 +54,21 @@ fun MessageInputField( onSendClick: () -> Unit, modifier: Modifier = Modifier, placeholder: String = "", + autoFocus: Boolean = false, ){ val maxLength=10 val focusManager= LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + val focusRequester = remember { + FocusRequester() + } val canSend=value.isNotBlank() && value.length <= maxLength + LaunchedEffect(autoFocus) { + if (autoFocus) { + focusRequester.requestFocus() + keyboardController?.show() + } + } Row( modifier=modifier @@ -74,7 +89,9 @@ fun MessageInputField( BasicTextField( value = value, onValueChange = onValueChange, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .focusRequester(focusRequester), singleLine = true, textStyle = CallTheme.typography.bodySmallBold.copy( color = Gray900, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt index 51592b2f..830069a1 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt @@ -1,6 +1,7 @@ package kr.co.call.impl.component import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -23,23 +24,30 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.theme.Gray100 +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.Gray900 import kr.co.call.designsystem.theme.SubRed @Composable fun NameBox( - modifier: Modifier, - label:String, + label: String, value: String, - onValueChange: (String) ->Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, ){ Column( modifier=modifier .width(125.dp) ){ Row { - Text(text=label) + Text(text=label, + style=CallTheme.typography.bodyMediumMedium, + color= Gray600, + ) Text(text=" *", - color= SubRed + style=CallTheme.typography.bodyMediumMedium, + color= SubRed, ) } Spacer(modifier=Modifier.height(6.dp)) @@ -51,16 +59,14 @@ fun NameBox( color=Gray100, shape=RoundedCornerShape(10.dp) ) + .clickable(onClick=onClick) .padding(horizontal=16.dp), contentAlignment= Alignment.CenterStart ) { - BasicTextField( - value = value, - onValueChange = onValueChange, - singleLine = true, - textStyle = CallTheme.typography.bodyMediumMedium, - modifier = Modifier - .fillMaxWidth() + Text( + text = value.ifBlank { "$label 입력" }, + color = if (value.isBlank()) Gray400 else Gray900, + style = CallTheme.typography.bodyMediumMedium, ) } } @@ -68,18 +74,13 @@ fun NameBox( @Preview( showBackground = true, - backgroundColor = 0xFFFFFFFF + backgroundColor = 0xFFFFFFFF, ) @Composable private fun NameBoxPreview() { - var name by remember { - mutableStateOf("") - } - NameBox( - modifier = Modifier, label = "이름", - value = name, - onValueChange = { name = it }, + value = "수현", + onClick = {}, ) } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt index 27bc8e7d..b162b296 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt @@ -91,7 +91,7 @@ fun TopTitle( ) } } - Spacer(modifier = Modifier.height(3.dp)) + Spacer(modifier = Modifier.height(5.dp)) Text( text = description, style = CallTheme.typography.bodySmall, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index d122074b..94122037 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -2,11 +2,29 @@ package kr.co.call.impl.entry import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey -import kr.co.call.api.OnboardingNavKey -import kr.co.call.impl.screen.OnboardingScreen +import kr.co.call.api.Onboarding1NavKey +import kr.co.call.api.Onboarding2NavKey +import kr.co.call.impl.screen.Onboarding1Screen +import kr.co.call.impl.screen.Onboarding2Screen +import java.time.LocalDate -fun EntryProviderScope.onboardingEntry() { - entry { - OnboardingScreen() +fun EntryProviderScope.onboardingEntry( + onProfileClick:()->Unit, + onNextClick:( + lastName: String, + firstName: String, + birthday: LocalDate, + job: String, + mbti: String, + )->Unit, +) { + entry { + Onboarding1Screen( + onProfileClick=onProfileClick, + onNextClick=onNextClick + ) + } + entry{ + Onboarding2Screen() } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index 7d2db8fa..f4ae81f6 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -1,4 +1,268 @@ package kr.co.call.impl.screen -class Onboarding1Screen { +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import java.time.LocalDate +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.impl.component.BirthdayChoice +import kr.co.call.impl.component.MemberChoice +import kr.co.call.impl.component.MessageInputField +import kr.co.call.impl.component.NameBox +import kr.co.call.impl.component.ProfileChoice +import kr.co.call.impl.component.TopTitle + +private enum class EditingNameField { + LAST_NAME, + FIRST_NAME, +} +@Composable +fun Onboarding1Screen ( + onProfileClick:()->Unit, + onNextClick: ( + lastName: String, + firstName: String, + birthday: LocalDate, + job: String, + mbti: String, + )->Unit, + modifier: Modifier =Modifier, + profileImageURl: String?=null, +) { + var lastName by rememberSaveable { + mutableStateOf("") + } + var firstName by rememberSaveable { + mutableStateOf("") + } + var birthday by remember { + mutableStateOf(LocalDate.of(2001, 5, 18)) + } + var job by rememberSaveable { + mutableStateOf("") + } + var mbti by rememberSaveable { + mutableStateOf("") + } + var editingNameField by rememberSaveable { + mutableStateOf(null) + } + + var nameInput by rememberSaveable { + mutableStateOf("") + } + val canMoveNext = + lastName.isNotBlank() && firstName.isNotBlank() && job.isNotBlank() + Box( + modifier = modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding() + .padding( + start = 24.dp, + end = 24.dp, + top = 54.dp, + bottom = 18.dp, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + TopTitle( + title = "먼저 나를\n소개해볼까요?", + description = "전화왔어에서 사용할 나의 프로필을 완성해주세요.", + ) + Spacer(modifier = Modifier.height(23.dp)) + Text( + text="사진선택", + style= CallTheme.typography.bodyMedium, + color= Gray600, + ) + Spacer(modifier = Modifier.height(18.dp)) + ProfileChoice( + modifier = Modifier, + imageUrl = profileImageURl, + onClick = onProfileClick, + size = 99.dp, + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(18.dp), + ) { + NameBox( + label = "성", + value = lastName, + onClick = { + nameInput = lastName + editingNameField = EditingNameField.LAST_NAME + }, + ) + + NameBox( + label = "이름", + value = firstName, + onClick = { + nameInput = firstName + editingNameField = EditingNameField.FIRST_NAME + }, + ) + } + Spacer(modifier = Modifier.height(24.dp)) + BirthdayChoice( + selectedDate = birthday, + onDateSelected = { newDate -> + birthday = newDate + }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(24.dp)) + MemberChoice( + modifier = Modifier.fillMaxWidth(), + label = "직업", + selectedOption = job, + placeholder = "직업을 선택해주세요", + options = listOf("대학생", "직장인", "기타",), + onOptionSelected = { job = it }, + required = true, + ) + Spacer(modifier = Modifier.height(24.dp)) + MemberChoice( + modifier = Modifier.fillMaxWidth(), + label = "MBTI", + selectedOption = mbti, + placeholder = "MBTI를 선택해주세요", + options = listOf( + "INFP", "INFJ", "ENFP", "ENFJ", + "INTP", "INTJ", "ENTP", "ENTJ", + "ISFP", "ISFJ", "ESFP", "ESFJ", + "ISTP", "ISTJ", "ESTP", "ESTJ", + ), + onOptionSelected = { mbti = it }, + ) + Spacer(modifier = Modifier.height(23.dp)) + Text( + text = "상세 정보는 나중에 다시 수정할 수 있어요!", + modifier = Modifier + .fillMaxWidth() + .padding(bottom=5.dp), + color = Gray400, + style = CallTheme.typography.bodySmall, + textAlign = TextAlign.Center, + ) + } + + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "다음", + enabled = canMoveNext, + onClick = { + onNextClick( + lastName, + firstName, + birthday, + job, + mbti, + ) + }, + ) + } + + if (editingNameField != null) { + // 배경 어둡게 처리 + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.15f)) + .clickable { + editingNameField = null + nameInput = "" + }, + ) + + // 키보드 바로 위 입력창 + MessageInputField( + value = nameInput, + onValueChange = { input -> + nameInput = input.take(10) + }, + onSendClick = { + when (editingNameField) { + EditingNameField.LAST_NAME -> { + lastName = nameInput + } + + EditingNameField.FIRST_NAME -> { + firstName = nameInput + } + null -> Unit + } + + editingNameField = null + nameInput = "" + }, + placeholder = when (editingNameField) { + EditingNameField.LAST_NAME -> "성을 입력해주세요." + EditingNameField.FIRST_NAME -> "이름을 입력해주세요." + null -> "" + }, + autoFocus = true, + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding() + .padding( + horizontal = 16.dp, + vertical = 13.dp, + ), + ) + } +} +} + +@Preview( + name = "Onboarding 1 Screen", + showBackground = true, + backgroundColor = 0xFFFFFFFF, +) +@Composable +private fun Onboarding1ScreenPreview() { + CallFromAiTheme { + Onboarding1Screen( + onProfileClick = {}, + onNextClick = { lastName, firstName, birthday, job, mbti -> + // Preview에서는 동작하지 않음 + }, + profileImageURl = null, + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index d07b356f..405d53ff 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -1,4 +1,4 @@ package kr.co.call.impl.screen -class Onboarding2Screen { +fun Onboarding2Screen() { } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt new file mode 100644 index 00000000..1792d022 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -0,0 +1,3 @@ +package kr.co.call.impl.screen + +fun Onboarding5screen(){} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt deleted file mode 100644 index f3bd9d77..00000000 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/OnboardingScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package kr.co.call.impl.screen - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun OnboardingScreen( - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Text("온보딩 화면") - } -} diff --git a/feature/onboarding/impl/src/main/res/drawable/ic_calendar.xml b/feature/onboarding/impl/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 00000000..31fc5107 --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,13 @@ + + + From 96c8665d2da8a803ee967751f28e1300157a5b9a Mon Sep 17 00:00:00 2001 From: dada4 Date: Mon, 27 Jul 2026 06:18:34 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 29 +- .../java/kr/co/call/api/OnboardingRoute.kt | 5 +- .../co/call/impl/component/AgeInputField.kt | 115 ++++++++ .../kr/co/call/impl/component/BackTopBar.kt | 61 ++++ .../co/call/impl/component/KeywordChoice.kt | 5 +- .../kr/co/call/impl/component/MemberChoice.kt | 2 +- .../impl/component/RelatonshipChoiceCard.kt | 8 +- .../kr/co/call/impl/component/TopTitle.kt | 21 +- .../call/impl/entry/OnboardingEntryBuilder.kt | 68 ++++- .../co/call/impl/screen/Onboarding1Screen.kt | 39 +-- .../co/call/impl/screen/Onboarding2Screen.kt | 271 +++++++++++++++++- .../co/call/impl/screen/Onboarding3Screen.kt | 209 +++++++++++++- .../co/call/impl/screen/Onboarding4Screen.kt | 136 ++++++++- .../co/call/impl/screen/Onboarding5Screen.kt | 113 +++++++- .../co/call/impl/screen/Onboarding6Screen.kt | 7 + .../impl/src/main/res/drawable/ic_back.xml | 13 + 16 files changed, 1057 insertions(+), 45 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt create mode 100644 feature/onboarding/impl/src/main/res/drawable/ic_back.xml diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index cdfb2fce..769615ba 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -29,6 +29,9 @@ import kr.co.call.api.ManagerChatRoomNayKey import kr.co.call.api.MyPageNavKey import kr.co.call.api.Onboarding1NavKey import kr.co.call.api.Onboarding2NavKey +import kr.co.call.api.Onboarding3NavKey +import kr.co.call.api.Onboarding4NavKey +import kr.co.call.api.Onboarding5NavKey import kr.co.call.api.TermNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab @@ -110,9 +113,33 @@ fun AppScreen(modifier: Modifier = Modifier) { onProfileClick = { // 사진 선택 기능은 나중에 구현 }, - onNextClick = { _, _, _, _, _ -> + onOnboarding1Next= { _, _, _, _, _ -> appNavigator.navigate(Onboarding2NavKey) }, + onBackFromOnboarding2 = { + appNavigator.popBackStack() + }, + onOnboarding2Next = { _, _, _, _, _ -> + appNavigator.navigate(Onboarding3NavKey) + }, + onBackFromOnboarding3 = { + appNavigator.popBackStack() + }, + onOnboarding3Next = { _, _, _ -> + appNavigator.navigate(Onboarding4NavKey) + }, + onBackFromOnboarding4 = { + appNavigator.popBackStack() + }, + onOnboarding4Next = { _ -> + appNavigator.navigate(Onboarding5NavKey) + }, + onBackFromOnboarding5 = { + appNavigator.popBackStack() + }, + onOnboarding5Next = { _ -> + // 온보딩 완료 후 홈화면으로 화면으로 이동 + }, ) homeEntry() chattingEntry( diff --git a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt index 53f2174d..9463577f 100644 --- a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt +++ b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt @@ -14,5 +14,8 @@ data object Onboarding2NavKey: NavKey { data object Onboarding3NavKey: NavKey @Serializable -data object Onboarding4NavKey +data object Onboarding4NavKey: NavKey + +@Serializable +data object Onboarding5NavKey: NavKey diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt new file mode 100644 index 00000000..ac3b4642 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/AgeInputField.kt @@ -0,0 +1,115 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray100 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.Gray900 +import kr.co.call.designsystem.theme.SubRed +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue + +@Composable +fun AgeInputField( + age: String, + onAgeChange: (String) -> Unit, + modifier: Modifier = Modifier, +){ + Column(modifier = modifier) { + Row { + Text( + text = "만 나이", + style = CallTheme.typography.bodyMedium, + color = Gray600, + ) + Text( + text = "*", + style = CallTheme.typography.bodyMedium, + color = SubRed, + ) + } + + Spacer(modifier = Modifier.height(7.dp)) + + BasicTextField( + value = age, + onValueChange = { input -> + if (input.all(Char::isDigit)) onAgeChange(input) + }, + modifier = Modifier + .fillMaxWidth() + .height(51.dp) + .background( + color = Gray100, + shape = RoundedCornerShape(10.dp), + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + textStyle = CallTheme.typography.bodyMediumMedium.copy( + color = Gray900, + ), + decorationBox = { innerTextField -> + Row( + modifier = Modifier + .fillMaxSize() + .padding( + start = 19.dp, + top = 16.dp, + end = 15.dp, + bottom = 15.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.weight(1f)) { + innerTextField() + } + Text( + text = "(세)", + style = CallTheme.typography.bodyMedium, + color = Gray600, + ) + } + }, + ) + } +} + +@Preview( + name = "AgeInputField", + showBackground = true, + widthDp = 393, +) +@Composable +private fun AgeInputFieldPreview() { + var age by rememberSaveable { mutableStateOf("25") } + CallFromAiTheme { + AgeInputField( + age = age, + onAgeChange = { age = it }, + modifier = Modifier.padding(16.dp), + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt new file mode 100644 index 00000000..dd5d154e --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt @@ -0,0 +1,61 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.layout.Row +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.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.theme.Black +import kr.co.call.onboarding.impl.R + +@Composable +fun BackTopBar( + onBackClick:()->Unit, + modifier: Modifier =Modifier, +){ + Row( + modifier=modifier + .fillMaxWidth() + .height(58.dp) + .padding( + start=0.dp, + end=8.dp,), + verticalAlignment = Alignment.CenterVertically, + ){ + IconButton( + onClick=onBackClick, + ){ + Icon( + painter= painterResource(R.drawable.ic_back), + contentDescription="뒤로가기", + tint= Black, + modifier=Modifier + .size( + width=8.dp, + height=15.dp + ) + ) + } + } +} + +@Preview( + showBackground = true, + widthDp = 393, + heightDp=58 +) +@Composable +private fun BackTopBarPreview() { + BackTopBar( + onBackClick = {} + ) +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt index ed4d3ae6..ce08fbab 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -73,7 +74,9 @@ fun KeywordChoice( modifier=modifier, ){ Surface( - modifier=Modifier.toggleable( + modifier=Modifier + .height(33.dp) + .toggleable( value = selectedOrder !=null, onValueChange={ onClick() diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt index ea53ac70..66497f40 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -87,7 +87,7 @@ fun MemberChoice( Surface( modifier=Modifier .fillMaxWidth() - .height(39.dp) + .height(51.dp) .clickable( role= Role.Button, onClick={ diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt index a6b8dd42..4379a067 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt @@ -47,8 +47,8 @@ fun RelationshipChoiceCard( Surface( modifier = modifier .size( - width = 116.dp, - height = 185.dp, + width = 106.dp, + height = 162.dp, ) .selectable( selected=selected, @@ -70,8 +70,8 @@ fun RelationshipChoiceCard( modifier= Modifier .fillMaxSize() .padding( - horizontal=12.dp, - vertical=26.dp, + horizontal=6.dp, + vertical=13.dp, ), horizontalAlignment=Alignment.CenterHorizontally, ){ diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt index b162b296..d2a6725b 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kr.co.call.designsystem.theme.Black import kr.co.call.designsystem.theme.CallFromAiTheme @@ -34,6 +35,7 @@ fun TopTitle( description: String, currentStep: Int?=null, totalStep: Int?=null, + horizontalPadding: Dp =14.dp, ) { val progress = if ( currentStep != null && totalStep != null && totalStep > 0 @@ -56,12 +58,12 @@ fun TopTitle( Box( modifier= Modifier .fillMaxWidth(progress) - .fillMaxHeight() .clip( RoundedCornerShape( topEnd = 100.dp, bottomEnd = 100.dp), ) + .fillMaxHeight() .background(brush= MainGradient), ) } @@ -72,7 +74,9 @@ fun TopTitle( Row( modifier = Modifier .fillMaxWidth() - .padding(end = 14.dp), + .padding( + start=horizontalPadding, + end = horizontalPadding), verticalAlignment = Alignment.Top, ) { Text( @@ -91,12 +95,13 @@ fun TopTitle( ) } } - Spacer(modifier = Modifier.height(5.dp)) - Text( - text = description, - style = CallTheme.typography.bodySmall, - color = Gray400, - ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = description, + modifier=Modifier.padding(start=horizontalPadding), + style = CallTheme.typography.bodySmall, + color = Gray400, + ) } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index 94122037..8e53520f 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -4,27 +4,81 @@ import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import kr.co.call.api.Onboarding1NavKey import kr.co.call.api.Onboarding2NavKey +import kr.co.call.api.Onboarding3NavKey +import kr.co.call.api.Onboarding4NavKey +import kr.co.call.api.Onboarding5NavKey import kr.co.call.impl.screen.Onboarding1Screen import kr.co.call.impl.screen.Onboarding2Screen +import kr.co.call.impl.screen.Onboarding3Screen +import kr.co.call.impl.screen.Onboarding4Screen +import kr.co.call.impl.screen.Onboarding5Screen +import kr.co.call.impl.screen.Relationship +import kr.co.call.impl.screen.SpeechStyle import java.time.LocalDate +import kr.co.call.impl.component.PreferTime +import kr.co.call.impl.component.Trait fun EntryProviderScope.onboardingEntry( - onProfileClick:()->Unit, - onNextClick:( + onProfileClick: () -> Unit, + onOnboarding1Next: ( lastName: String, firstName: String, birthday: LocalDate, job: String, mbti: String, - )->Unit, + ) -> Unit, + onBackFromOnboarding2: () -> Unit, + onOnboarding2Next: ( + age: String, + lastName: String, + firstName: String, + job: String, + mbti: String, + ) -> Unit, + onBackFromOnboarding3: () -> Unit, + onOnboarding3Next: ( + speechStyle: SpeechStyle, + relationship: Relationship, + temperature: Int, + ) -> Unit, + onBackFromOnboarding4: () -> Unit, + onOnboarding4Next: (List) -> Unit, + onBackFromOnboarding5: () -> Unit, + onOnboarding5Next: (PreferTime) -> Unit, ) { entry { Onboarding1Screen( - onProfileClick=onProfileClick, - onNextClick=onNextClick + onProfileClick = onProfileClick, + onNextClick = onOnboarding1Next, ) } - entry{ - Onboarding2Screen() + + entry { + Onboarding2Screen( + onBackClick = onBackFromOnboarding2, + onProfileClick = onProfileClick, + onNextClick = onOnboarding2Next, + ) + } + + entry { + Onboarding3Screen( + onBackClick = onBackFromOnboarding3, + onNextClick = onOnboarding3Next, + ) + } + + entry { + Onboarding4Screen( + onBackClick = onBackFromOnboarding4, + onNextClick = onOnboarding4Next, + ) + } + + entry { + Onboarding5Screen( + onBackClick = onBackFromOnboarding5, + onNextClick = onOnboarding5Next, + ) } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index f4ae81f6..567051fd 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -88,8 +88,8 @@ fun Onboarding1Screen ( .fillMaxSize() .navigationBarsPadding() .padding( - start = 24.dp, - end = 24.dp, + start = 27.dp, + end = 27.dp, top = 54.dp, bottom = 18.dp, ), @@ -101,6 +101,7 @@ fun Onboarding1Screen ( TopTitle( title = "먼저 나를\n소개해볼까요?", description = "전화왔어에서 사용할 나의 프로필을 완성해주세요.", + horizontalPadding = 0.dp, ) Spacer(modifier = Modifier.height(23.dp)) Text( @@ -170,7 +171,10 @@ fun Onboarding1Screen ( ), onOptionSelected = { mbti = it }, ) - Spacer(modifier = Modifier.height(23.dp)) + } + Column( + modifier=Modifier.fillMaxWidth(), + ) { Text( text = "상세 정보는 나중에 다시 수정할 수 있어요!", modifier = Modifier @@ -180,22 +184,21 @@ fun Onboarding1Screen ( style = CallTheme.typography.bodySmall, textAlign = TextAlign.Center, ) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "다음", + enabled = canMoveNext, + onClick = { + onNextClick( + lastName, + firstName, + birthday, + job, + mbti, + ) + }, + ) } - - SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = "다음", - enabled = canMoveNext, - onClick = { - onNextClick( - lastName, - firstName, - birthday, - job, - mbti, - ) - }, - ) } if (editingNameField != null) { diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index 405d53ff..4291243c 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -1,4 +1,273 @@ package kr.co.call.impl.screen -fun Onboarding2Screen() { +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.White +import kr.co.call.impl.component.AgeInputField +import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.MemberChoice +import kr.co.call.impl.component.MessageInputField +import kr.co.call.impl.component.NameBox +import kr.co.call.impl.component.ProfileChoice +import kr.co.call.impl.component.TopTitle + + +private enum class Onboarding2EditingNameField { + LAST_NAME, + FIRST_NAME, +} +@Composable +fun Onboarding2Screen( + onBackClick: () -> Unit, + onProfileClick: () -> Unit, + onNextClick: ( + age: String, + lastName: String, + firstName: String, + job: String, + mbti: String, + ) -> Unit, + modifier: Modifier = Modifier, + profileImageUrl: String? = null, +) { + var age by rememberSaveable { mutableStateOf("") } + var lastName by rememberSaveable { mutableStateOf("") } + var firstName by rememberSaveable { mutableStateOf("") } + var job by rememberSaveable { mutableStateOf("") } + var mbti by rememberSaveable { mutableStateOf("") } + + var editingNameField by rememberSaveable { + mutableStateOf(null) + } + var nameInput by rememberSaveable { mutableStateOf("") } + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + val canMoveNext = age.isNotBlank() && + lastName.isNotBlank() && + firstName.isNotBlank() && + job.isNotBlank() + + Box( + modifier = modifier + .fillMaxSize() + .background(White), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + ) { + BackTopBar(onBackClick = onBackClick) + + TopTitle( + title = "내 이상형 설정,\n4단계면 끝나요", + description = "함께할 상대방의 기본 정보를 입력해주세요.", + currentStep = 1, + totalStep = 4, + horizontalPadding = 27.dp, + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 27.dp), + ) { + Spacer(modifier = Modifier.height(25.dp)) + + Text( + text = "사진 선택", + style = CallTheme.typography.bodyMedium, + color = Gray600, + ) + + Spacer(modifier = Modifier.height(18.dp)) + + ProfileChoice( + modifier = Modifier, + imageUrl = profileImageUrl, + onClick = onProfileClick, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + AgeInputField( + age = age, + onAgeChange = { age = it }, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(18.dp), + ) { + NameBox( + label = "성", + value = lastName, + onClick = { + nameInput = lastName + editingNameField = Onboarding2EditingNameField.LAST_NAME + }, + ) + + NameBox( + label = "이름", + value = firstName, + onClick = { + nameInput = firstName + editingNameField = Onboarding2EditingNameField.FIRST_NAME + }, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + MemberChoice( + modifier = Modifier.fillMaxWidth(), + label = "직업", + selectedOption = job, + placeholder = "직업을 선택해주세요", + options = listOf("학생", "직장인", "프리랜서", "기타"), + onOptionSelected = { job = it }, + required = true, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + MemberChoice( + modifier = Modifier.fillMaxWidth(), + label = "MBTI", + selectedOption = mbti, + placeholder = "MBTI를 선택해주세요", + options = listOf( + "INFP", "INFJ", "ENFP", "ENFJ", + "INTP", "INTJ", "ENTP", "ENTJ", + "ISFP", "ISFJ", "ESFP", "ESFJ", + "ISTP", "ISTJ", "ESTP", "ESTJ", + ), + onOptionSelected = { mbti = it }, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .imePadding() + .padding(horizontal = 27.dp, vertical = 18.dp), + ) { + Text( + text = "상세 정보는 나중에 다시 수정할 수 있어요!", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = CallTheme.typography.caption, + color = Gray400, + ) + + Spacer(modifier = Modifier.height(9.dp)) + + SecondaryButton( + text = "다음", + enabled = canMoveNext, + onClick = { + onNextClick( + age, + lastName, + firstName, + job, + mbti, + ) + }, + ) + } + } + + if (editingNameField != null) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.15f)) + .clickable { + editingNameField = null + nameInput = "" + focusManager.clearFocus() + keyboardController?.hide() + }, + ) + + MessageInputField( + value = nameInput, + onValueChange = { nameInput = it.take(10) }, + onSendClick = { + when (editingNameField) { + Onboarding2EditingNameField.LAST_NAME -> lastName = nameInput + Onboarding2EditingNameField.FIRST_NAME -> firstName = nameInput + null -> Unit + } + + editingNameField = null + nameInput = "" + focusManager.clearFocus() + keyboardController?.hide() + }, + placeholder = when (editingNameField) { + Onboarding2EditingNameField.LAST_NAME -> "성을 입력해주세요." + Onboarding2EditingNameField.FIRST_NAME -> "이름을 입력해주세요." + null -> "" + }, + autoFocus = true, + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding() + .padding(horizontal = 16.dp, vertical = 13.dp), + ) + } + } +} + +@Preview( + name = "Onboarding 2 Screen", + showBackground = true, + widthDp = 393, + heightDp = 852, +) +@Composable +private fun Onboarding2ScreenPreview() { + CallFromAiTheme { + Onboarding2Screen( + onBackClick = {}, + onProfileClick = {}, + onNextClick = { _, _, _, _, _ -> }, + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt index adb6d1d5..f7c553ef 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt @@ -1,4 +1,211 @@ package kr.co.call.impl.screen -class Onboarding3Screen { +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.SubGray2 +import kr.co.call.designsystem.theme.White +import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.RelationshipChoiceCard +import kr.co.call.impl.component.SpeechChoice +import kr.co.call.impl.component.TempControl +import kr.co.call.impl.component.TopTitle +import kr.co.call.onboarding.impl.R + +enum class SpeechStyle{ + CASUAL, + SEMI_FORMAL, + FORMAL +} + +enum class Relationship{ + SOME, + EARLY_DATING, + LONG_TERM +} + +@Composable +fun Onboarding3Screen( + onBackClick:()->Unit, + onNextClick: ( + speechStyle: SpeechStyle, + relationship: Relationship, + temperature: Int, + ) -> Unit, + modifier: Modifier =Modifier, +) { + var selectedSpeech by rememberSaveable { + mutableStateOf(null) + } + var selectedRelationship by rememberSaveable { + mutableStateOf(null) + } + var temperature by rememberSaveable { + mutableIntStateOf(50) + } + + val canMoveNext = + selectedSpeech != null && selectedRelationship != null + Column( + modifier = modifier + .fillMaxSize() + .background(White) + .navigationBarsPadding(), + ) { + BackTopBar( + onBackClick = onBackClick, + ) + TopTitle( + title = "어떤 사이로\n시작할까요?", + description = "원하는 상대방과의 관계를 선택해주세요.", + currentStep = 2, + totalStep = 4, + horizontalPadding = 27.dp, + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 27.dp) + .background( + SubGray2 + ) + ) { + Spacer(modifier = Modifier.height(26.dp)) + + androidx.compose.material3.Text( + text = "어떤 말투로 대화할까요?", + style = CallTheme.typography.bodySmall, + color = Gray400, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(17.dp), + ) { + SpeechChoice( + text = "반말", + selected = selectedSpeech == SpeechStyle.CASUAL, + onClick = { selectedSpeech = SpeechStyle.CASUAL }, + ) + SpeechChoice( + text = "반존대", + selected = selectedSpeech == SpeechStyle.SEMI_FORMAL, + onClick = { selectedSpeech = SpeechStyle.SEMI_FORMAL }, + ) + SpeechChoice( + text = "존댓말", + selected = selectedSpeech == SpeechStyle.FORMAL, + onClick = { selectedSpeech = SpeechStyle.FORMAL }, + ) + } + + Spacer(modifier = Modifier.height(26.dp)) + + androidx.compose.material3.Text( + text = "어떤 관계로 시작할까요?", + style = CallTheme.typography.bodySmall, + color = Gray400, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + RelationshipChoiceCard( + imageRes = R.drawable.relationship_some, + title = "썸", + description = "설레는 시작", + selected = selectedRelationship == Relationship.SOME, + onClick = { selectedRelationship = Relationship.SOME }, + ) + RelationshipChoiceCard( + imageRes = R.drawable.relationship_first, + title = "연애 초기", + description = "달달한 사이", + selected = selectedRelationship == Relationship.EARLY_DATING, + onClick = { selectedRelationship = Relationship.EARLY_DATING }, + ) + RelationshipChoiceCard( + imageRes = R.drawable.relationship_old, + title = "오래된 연인", + description = "편안한 관계", + selected = selectedRelationship == Relationship.LONG_TERM, + onClick = { selectedRelationship = Relationship.LONG_TERM }, + ) + } + + Spacer(modifier = Modifier.height(25.dp)) + + TempControl( + modifier = Modifier.fillMaxWidth(), + onTemperatureChange = { temperature = it }, + ) + } + Spacer(modifier = Modifier.weight(1f)) + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .background( + SubGray2 + ) + .padding( + start = 27.dp, + end = 27.dp, + bottom = 18.dp, + ), + text = "다음", + enabled = canMoveNext, + onClick = { + onNextClick( + selectedSpeech!!, + selectedRelationship!!, + temperature, + ) + }, + ) + } +} + +@Preview( + name = "Onboarding 3 Screen", + showBackground = true, + widthDp = 393, + heightDp = 852, +) +@Composable +private fun Onboarding3ScreenPreview() { + CallFromAiTheme { + Onboarding3Screen( + onBackClick = {}, + onNextClick = { _, _, _ -> + // Preview에서는 화면 이동을 하지 않습니다. + }, + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index 3f72750c..17489862 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -1,4 +1,138 @@ package kr.co.call.impl.screen -class Onboarding4Screen { +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import kr.co.call.impl.component.Trait +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.PrimaryButton +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.SubGray2 +import kr.co.call.designsystem.theme.White +import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.KeywordChoice +import kr.co.call.impl.component.TopTitle + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun Onboarding4Screen( + onBackClick: () -> Unit, + onNextClick: (List) -> Unit, + initialSelectedKeywords: List = emptyList(), + modifier: Modifier = Modifier, +) { + var selectedKeywords by rememberSaveable(initialSelectedKeywords) { + mutableStateOf(initialSelectedKeywords) + } + + val selectedTraits = selectedKeywords.mapNotNull(Trait::fromKeyword) + + Column( + modifier = modifier + .fillMaxSize() + .background(White), + ) { + BackTopBar(onBackClick = onBackClick) + + TopTitle( + title = "어떤 매력에\n끌리나요?", + description = "우선 순위에 따라 최대 5개를 선택해주세요.", + currentStep = 3, + totalStep = 4, + horizontalPadding = 27.dp, + ) + Spacer(modifier=Modifier.height(5.dp)) + + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(SubGray2) + .navigationBarsPadding(), + ) { + FlowRow( + modifier = Modifier.padding( + start = 27.dp, + end = 27.dp, + top = 20.dp, + ), + horizontalArrangement = Arrangement.spacedBy(15.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + maxItemsInEachRow = 3, + ) { + Trait.entries.forEach { trait -> + val selectedOrder = selectedKeywords + .indexOf(trait.keyword) + .takeIf { it >= 0 } + ?.plus(1) + + KeywordChoice( + emoji = trait.emoji, + text = trait.label, + selectedOrder = selectedOrder, + onClick = { + selectedKeywords = when { + trait.keyword in selectedKeywords -> + selectedKeywords - trait.keyword + + selectedKeywords.size < 5 -> + selectedKeywords + trait.keyword + + else -> selectedKeywords + } + }, + ) + } + } + Spacer(modifier = Modifier.weight(1f)) + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 27.dp, + end = 27.dp, + bottom = 18.dp, + ), + text = "다음", + onClick = { onNextClick(selectedTraits) }, + ) + } + } +} + +@Preview( + name = "Onboarding 4 - 선택됨", + showBackground = true, + widthDp = 393, + heightDp = 852, +) +@Composable +private fun Onboarding4ScreenPreview() { + CallFromAiTheme { + Onboarding4Screen( + onBackClick = {}, + onNextClick = {}, + initialSelectedKeywords = listOf( + Trait.HUMOROUS.keyword, + Trait.PLAYFUL.keyword, + Trait.AFFECTIONATE.keyword, + ), + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt index 1792d022..5f555951 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -1,3 +1,114 @@ package kr.co.call.impl.screen -fun Onboarding5screen(){} \ No newline at end of file +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.theme.SubGray2 +import kr.co.call.designsystem.theme.White +import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.PreferTime +import kr.co.call.impl.component.TopTitle +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import kr.co.call.designsystem.theme.CallFromAiTheme + +@Composable +fun Onboarding5Screen( + onBackClick: () -> Unit, + onNextClick: (PreferTime) -> Unit, + initialPreferTime: PreferTime = PreferTime.DAY, + modifier: Modifier = Modifier, +){ + var selectedPreferTime by rememberSaveable { + mutableStateOf(initialPreferTime) + } + Column( + modifier = modifier + .fillMaxSize() + .padding( + top = 54.dp, + bottom = 18.dp, + ) + .background(White), + ) { + BackTopBar(onBackClick = onBackClick) + + TopTitle( + title = "언제 먼저 통화를\n걸어드리면 좋을까요?", + description = "편하게 통화하기 좋은 시간대를 선택해주세요.", + currentStep = 4, + totalStep = 4, + horizontalPadding = 27.dp, + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(SubGray2) + .navigationBarsPadding(), + ) { + Column( + modifier = Modifier.padding( + start = 27.dp, + end = 27.dp, + top = 26.dp, + ), + verticalArrangement = Arrangement.spacedBy(22.dp), + ) { + PreferTime.entries + .forEach { preferTime -> + PreferTime( + preferTime = preferTime, + selected = selectedPreferTime == preferTime, + onClick = { selectedPreferTime = preferTime }, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + SecondaryButton( + modifier = Modifier.padding( + start = 27.dp, + end = 27.dp, + bottom = 18.dp, + ), + text = "다음", + onClick = { onNextClick(selectedPreferTime) }, + ) + } + } +} + +@Preview( + name = "Onboarding 5", + showBackground = true, + widthDp = 393, + heightDp = 852, +) +@Composable +private fun Onboarding5ScreenPreview() { + CallFromAiTheme { + Onboarding5Screen( + onBackClick = {}, + onNextClick = {}, + initialPreferTime = PreferTime.DAY, + ) + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt new file mode 100644 index 00000000..1a6bea6c --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.screen + +fun Onboarding6Screen( + +){ + +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/res/drawable/ic_back.xml b/feature/onboarding/impl/src/main/res/drawable/ic_back.xml new file mode 100644 index 00000000..ff9ee976 --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/ic_back.xml @@ -0,0 +1,13 @@ + + + From c8d63f787c663bf2e86275a33da439af0a5a7351 Mon Sep 17 00:00:00 2001 From: dada4 Date: Tue, 28 Jul 2026 00:57:39 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 20 +- .../java/kr/co/call/api/OnboardingRoute.kt | 3 + .../{BackTopBar.kt => BackStepBar.kt} | 5 +- .../co/call/impl/component/BirthdayChoice.kt | 2 +- .../java/kr/co/call/impl/component/NameBox.kt | 2 +- .../impl/component/RelatonshipChoiceCard.kt | 6 +- .../call/impl/entry/OnboardingEntryBuilder.kt | 13 ++ .../co/call/impl/screen/Onboarding1Screen.kt | 2 +- .../co/call/impl/screen/Onboarding2Screen.kt | 6 +- .../co/call/impl/screen/Onboarding3Screen.kt | 197 +++++++++-------- .../co/call/impl/screen/Onboarding4Screen.kt | 4 +- .../co/call/impl/screen/Onboarding5Screen.kt | 10 +- .../co/call/impl/screen/Onboarding6Screen.kt | 206 +++++++++++++++++- .../main/res/drawable/onboarding_check.png | Bin 16956 -> 17985 bytes 14 files changed, 359 insertions(+), 117 deletions(-) rename feature/onboarding/impl/src/main/java/kr/co/call/impl/component/{BackTopBar.kt => BackStepBar.kt} (95%) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 769615ba..8db0e254 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -12,7 +12,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged @@ -32,6 +34,7 @@ import kr.co.call.api.Onboarding2NavKey import kr.co.call.api.Onboarding3NavKey import kr.co.call.api.Onboarding4NavKey import kr.co.call.api.Onboarding5NavKey +import kr.co.call.api.Onboarding6NavKey import kr.co.call.api.TermNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab @@ -82,7 +85,10 @@ fun AppScreen(modifier: Modifier = Modifier) { val bottomBarPadding = remember(bottomBarHeightPx) { with(density) { bottomBarHeightPx.toDp() } } - + //온보딩2에서 입력한 이름을 보관 + var aiFirstName by rememberSaveable { + mutableStateOf("") + } Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { @@ -119,7 +125,8 @@ fun AppScreen(modifier: Modifier = Modifier) { onBackFromOnboarding2 = { appNavigator.popBackStack() }, - onOnboarding2Next = { _, _, _, _, _ -> + onOnboarding2Next = { _, _,firstName, _, _ -> + aiFirstName=firstName appNavigator.navigate(Onboarding3NavKey) }, onBackFromOnboarding3 = { @@ -138,7 +145,14 @@ fun AppScreen(modifier: Modifier = Modifier) { appNavigator.popBackStack() }, onOnboarding5Next = { _ -> - // 온보딩 완료 후 홈화면으로 화면으로 이동 + appNavigator.navigate(Onboarding6NavKey) + }, + onboarding6FirstName = aiFirstName, + onOnboarding6CallNow = { + // TODO: 실제 통화 화면으로 이동 + }, + onOnboarding6CallLater = { + appNavigator.navigate(HomeNavKey) }, ) homeEntry() diff --git a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt index 9463577f..0de8a6d0 100644 --- a/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt +++ b/feature/onboarding/api/src/main/java/kr/co/call/api/OnboardingRoute.kt @@ -19,3 +19,6 @@ data object Onboarding4NavKey: NavKey @Serializable data object Onboarding5NavKey: NavKey +@Serializable +data object Onboarding6NavKey: NavKey + diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt similarity index 95% rename from feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt rename to feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt index dd5d154e..dde623d3 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackTopBar.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -18,7 +17,7 @@ import kr.co.call.designsystem.theme.Black import kr.co.call.onboarding.impl.R @Composable -fun BackTopBar( +fun BackStepBar( onBackClick:()->Unit, modifier: Modifier =Modifier, ){ @@ -55,7 +54,7 @@ fun BackTopBar( ) @Composable private fun BackTopBarPreview() { - BackTopBar( + BackStepBar( onBackClick = {} ) } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt index 51183ed9..4fa035ef 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -123,7 +123,7 @@ private fun LocalDate.toDisplayText(): String { private fun BirthdayChoicePreview() { CallFromAiTheme { BirthdayChoice( - selectedDate = LocalDate.of(2001, 5, 18), + selectedDate = LocalDate.of(2000, 1, 1), onDateSelected = {}, modifier = Modifier .fillMaxWidth() diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt index 830069a1..12fec4b0 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt @@ -64,7 +64,7 @@ fun NameBox( contentAlignment= Alignment.CenterStart ) { Text( - text = value.ifBlank { "$label 입력" }, + text = value.ifBlank { " " }, color = if (value.isBlank()) Gray400 else Gray900, style = CallTheme.typography.bodyMediumMedium, ) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt index 4379a067..ba004e1b 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/RelatonshipChoiceCard.kt @@ -47,8 +47,8 @@ fun RelationshipChoiceCard( Surface( modifier = modifier .size( - width = 106.dp, - height = 162.dp, + width = 110.dp, + height = 172.dp, ) .selectable( selected=selected, @@ -88,7 +88,7 @@ fun RelationshipChoiceCard( Text( text=title, - style= CallTheme.typography.bodyLargeBold.copy( + style= CallTheme.typography.bodyMediumBold.copy( brush=ChatGradient ), textAlign =TextAlign.Center, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index 8e53520f..6fb716e6 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -7,6 +7,7 @@ import kr.co.call.api.Onboarding2NavKey import kr.co.call.api.Onboarding3NavKey import kr.co.call.api.Onboarding4NavKey import kr.co.call.api.Onboarding5NavKey +import kr.co.call.api.Onboarding6NavKey import kr.co.call.impl.screen.Onboarding1Screen import kr.co.call.impl.screen.Onboarding2Screen import kr.co.call.impl.screen.Onboarding3Screen @@ -17,6 +18,7 @@ import kr.co.call.impl.screen.SpeechStyle import java.time.LocalDate import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.Trait +import kr.co.call.impl.screen.Onboarding6Screen fun EntryProviderScope.onboardingEntry( onProfileClick: () -> Unit, @@ -45,6 +47,9 @@ fun EntryProviderScope.onboardingEntry( onOnboarding4Next: (List) -> Unit, onBackFromOnboarding5: () -> Unit, onOnboarding5Next: (PreferTime) -> Unit, + onboarding6FirstName: String, + onOnboarding6CallNow: () -> Unit, + onOnboarding6CallLater: () -> Unit, ) { entry { Onboarding1Screen( @@ -81,4 +86,12 @@ fun EntryProviderScope.onboardingEntry( onNextClick = onOnboarding5Next, ) } + + entry { + Onboarding6Screen( + firstName = onboarding6FirstName, + onCallNowClick = onOnboarding6CallNow, + onCallLaterClick = onOnboarding6CallLater, + ) + } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index 567051fd..3a072154 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -63,7 +63,7 @@ fun Onboarding1Screen ( mutableStateOf("") } var birthday by remember { - mutableStateOf(LocalDate.of(2001, 5, 18)) + mutableStateOf(LocalDate.of(2000, 1, 1)) } var job by rememberSaveable { mutableStateOf("") diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index 4291243c..4f58afab 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -34,7 +34,7 @@ import kr.co.call.designsystem.theme.Gray400 import kr.co.call.designsystem.theme.Gray600 import kr.co.call.designsystem.theme.White import kr.co.call.impl.component.AgeInputField -import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.BackStepBar import kr.co.call.impl.component.MemberChoice import kr.co.call.impl.component.MessageInputField import kr.co.call.impl.component.NameBox @@ -88,7 +88,7 @@ fun Onboarding2Screen( modifier = Modifier .fillMaxSize(), ) { - BackTopBar(onBackClick = onBackClick) + BackStepBar(onBackClick = onBackClick) TopTitle( title = "내 이상형 설정,\n4단계면 끝나요", @@ -157,7 +157,7 @@ fun Onboarding2Screen( label = "직업", selectedOption = job, placeholder = "직업을 선택해주세요", - options = listOf("학생", "직장인", "프리랜서", "기타"), + options = listOf("학생", "직장인", "기타"), onOptionSelected = { job = it }, required = true, ) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt index f7c553ef..ed88564a 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kr.co.call.designsystem.component.button.SecondaryButton @@ -27,7 +28,7 @@ import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.theme.Gray400 import kr.co.call.designsystem.theme.SubGray2 import kr.co.call.designsystem.theme.White -import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.BackStepBar import kr.co.call.impl.component.RelationshipChoiceCard import kr.co.call.impl.component.SpeechChoice import kr.co.call.impl.component.TempControl @@ -74,7 +75,7 @@ fun Onboarding3Screen( .background(White) .navigationBarsPadding(), ) { - BackTopBar( + BackStepBar( onBackClick = onBackClick, ) TopTitle( @@ -84,111 +85,119 @@ fun Onboarding3Screen( totalStep = 4, horizontalPadding = 27.dp, ) + Spacer(modifier=Modifier.height(5.dp)) Column( modifier = Modifier .weight(1f) - .padding(horizontal = 27.dp) - .background( - SubGray2 - ) + .fillMaxWidth() + .background(SubGray2), ) { - Spacer(modifier = Modifier.height(26.dp)) - - androidx.compose.material3.Text( - text = "어떤 말투로 대화할까요?", - style = CallTheme.typography.bodySmall, - color = Gray400, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(17.dp), + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 27.dp), ) { - SpeechChoice( - text = "반말", - selected = selectedSpeech == SpeechStyle.CASUAL, - onClick = { selectedSpeech = SpeechStyle.CASUAL }, - ) - SpeechChoice( - text = "반존대", - selected = selectedSpeech == SpeechStyle.SEMI_FORMAL, - onClick = { selectedSpeech = SpeechStyle.SEMI_FORMAL }, - ) - SpeechChoice( - text = "존댓말", - selected = selectedSpeech == SpeechStyle.FORMAL, - onClick = { selectedSpeech = SpeechStyle.FORMAL }, - ) - } - - Spacer(modifier = Modifier.height(26.dp)) - - androidx.compose.material3.Text( - text = "어떤 관계로 시작할까요?", - style = CallTheme.typography.bodySmall, - color = Gray400, - ) + Spacer(modifier = Modifier.height(26.dp)) - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - RelationshipChoiceCard( - imageRes = R.drawable.relationship_some, - title = "썸", - description = "설레는 시작", - selected = selectedRelationship == Relationship.SOME, - onClick = { selectedRelationship = Relationship.SOME }, + androidx.compose.material3.Text( + text = "어떤 말투로 대화할까요?", + style = CallTheme.typography.bodySmall, + color = Gray400, ) - RelationshipChoiceCard( - imageRes = R.drawable.relationship_first, - title = "연애 초기", - description = "달달한 사이", - selected = selectedRelationship == Relationship.EARLY_DATING, - onClick = { selectedRelationship = Relationship.EARLY_DATING }, + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(17.dp), + ) { + SpeechChoice( + text = "반말", + selected = selectedSpeech == SpeechStyle.CASUAL, + onClick = { selectedSpeech = SpeechStyle.CASUAL }, + ) + SpeechChoice( + text = "반존대", + selected = selectedSpeech == SpeechStyle.SEMI_FORMAL, + onClick = { selectedSpeech = SpeechStyle.SEMI_FORMAL }, + ) + SpeechChoice( + text = "존댓말", + selected = selectedSpeech == SpeechStyle.FORMAL, + onClick = { selectedSpeech = SpeechStyle.FORMAL }, + ) + } + + Spacer(modifier = Modifier.height(26.dp)) + + androidx.compose.material3.Text( + text = "어떤 관계로 시작할까요?", + style = CallTheme.typography.bodySmall, + color = Gray400, ) - RelationshipChoiceCard( - imageRes = R.drawable.relationship_old, - title = "오래된 연인", - description = "편안한 관계", - selected = selectedRelationship == Relationship.LONG_TERM, - onClick = { selectedRelationship = Relationship.LONG_TERM }, + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + space=11.dp, + alignment=Alignment.CenterHorizontally + ), + ) { + RelationshipChoiceCard( + imageRes = R.drawable.relationship_some, + title = "썸", + description = "설레는 시작", + selected = selectedRelationship == Relationship.SOME, + onClick = { selectedRelationship = Relationship.SOME }, + ) + RelationshipChoiceCard( + imageRes = R.drawable.relationship_first, + title = "연애 초기", + description = "달달한 사이", + selected = selectedRelationship == Relationship.EARLY_DATING, + onClick = { selectedRelationship = Relationship.EARLY_DATING }, + ) + RelationshipChoiceCard( + imageRes = R.drawable.relationship_old, + title = "오래된 연인", + description = "편안한 관계", + selected = selectedRelationship == Relationship.LONG_TERM, + onClick = { selectedRelationship = Relationship.LONG_TERM }, + ) + } + + Spacer(modifier = Modifier.height(25.dp)) + + TempControl( + modifier = Modifier.fillMaxWidth(), + onTemperatureChange = { temperature = it }, ) } - - Spacer(modifier = Modifier.height(25.dp)) - - TempControl( - modifier = Modifier.fillMaxWidth(), - onTemperatureChange = { temperature = it }, + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .background( + SubGray2 + ) + .padding( + start = 27.dp, + end = 27.dp, + bottom = 18.dp, + ), + text = "다음", + enabled = canMoveNext, + onClick = { + onNextClick( + selectedSpeech!!, + selectedRelationship!!, + temperature, + ) + }, ) } - Spacer(modifier = Modifier.weight(1f)) - SecondaryButton( - modifier = Modifier - .fillMaxWidth() - .background( - SubGray2 - ) - .padding( - start = 27.dp, - end = 27.dp, - bottom = 18.dp, - ), - text = "다음", - enabled = canMoveNext, - onClick = { - onNextClick( - selectedSpeech!!, - selectedRelationship!!, - temperature, - ) - }, - ) } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index 17489862..e5164e7c 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -25,7 +25,7 @@ import kr.co.call.designsystem.component.button.SecondaryButton import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.SubGray2 import kr.co.call.designsystem.theme.White -import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.BackStepBar import kr.co.call.impl.component.KeywordChoice import kr.co.call.impl.component.TopTitle @@ -48,7 +48,7 @@ fun Onboarding4Screen( .fillMaxSize() .background(White), ) { - BackTopBar(onBackClick = onBackClick) + BackStepBar(onBackClick = onBackClick) TopTitle( title = "어떤 매력에\n끌리나요?", diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt index 5f555951..04adfadc 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.dp import kr.co.call.designsystem.component.button.SecondaryButton import kr.co.call.designsystem.theme.SubGray2 import kr.co.call.designsystem.theme.White -import kr.co.call.impl.component.BackTopBar +import kr.co.call.impl.component.BackStepBar import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.TopTitle import androidx.compose.runtime.getValue @@ -29,11 +29,11 @@ import kr.co.call.designsystem.theme.CallFromAiTheme fun Onboarding5Screen( onBackClick: () -> Unit, onNextClick: (PreferTime) -> Unit, - initialPreferTime: PreferTime = PreferTime.DAY, + initialPreferTime: PreferTime? = null, modifier: Modifier = Modifier, ){ var selectedPreferTime by rememberSaveable { - mutableStateOf(initialPreferTime) + mutableStateOf(null) } Column( modifier = modifier @@ -44,7 +44,7 @@ fun Onboarding5Screen( ) .background(White), ) { - BackTopBar(onBackClick = onBackClick) + BackStepBar(onBackClick = onBackClick) TopTitle( title = "언제 먼저 통화를\n걸어드리면 좋을까요?", @@ -90,7 +90,7 @@ fun Onboarding5Screen( bottom = 18.dp, ), text = "다음", - onClick = { onNextClick(selectedPreferTime) }, + onClick = { selectedPreferTime?.let(onNextClick) }, ) } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt index 1a6bea6c..80e7af7e 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt @@ -1,7 +1,211 @@ package kr.co.call.impl.screen -fun Onboarding6Screen( +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +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.fillMaxSize +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.button.PrimaryButton +import kr.co.call.designsystem.component.popup.TwoButtonPopup +import kr.co.call.designsystem.theme.Black +import kr.co.call.designsystem.theme.CallFromAiTheme +import kr.co.call.designsystem.theme.CallTheme +import kr.co.call.designsystem.theme.ChatGradient +import kr.co.call.designsystem.theme.Gray200 +import kr.co.call.designsystem.theme.Gray400 +import kr.co.call.designsystem.theme.Gray600 +import kr.co.call.designsystem.theme.Gray800 +import kr.co.call.designsystem.theme.MainVariant1 +import kr.co.call.designsystem.theme.MainVariant2 +import kr.co.call.designsystem.theme.SubPressed +import kr.co.call.designsystem.theme.White +import kr.co.call.onboarding.impl.R +@Composable +fun Onboarding6Screen( + firstName: String, + onCallNowClick:()->Unit, + onCallLaterClick:()->Unit, + modifier: Modifier =Modifier, ){ + var isCallNowPressed by remember {mutableStateOf(false)} + var isCallLaterPressed by remember {mutableStateOf(false)} + var showCallDialog by rememberSaveable {mutableStateOf(false)} + + Column( + modifier=modifier + .fillMaxSize() + .background(White) + .padding(horizontal = 16.dp), + horizontalAlignment =Alignment.CenterHorizontally, + ){ + Spacer(modifier=Modifier.height(188.dp)) + Image( + painter=painterResource(R.drawable.onboarding_check), + contentDescription = null, + modifier=Modifier.size(117.dp), + ) + Spacer(modifier=Modifier.height(22.dp)) + Text( + text="나의 이상형 등록 완료", + style=CallTheme.typography.titleMediumBold, + color= MainVariant1 + ) + Spacer(modifier=Modifier.height(16.dp)) + Text( + text="준비가 되었다면,\n지금 바로 첫 통화를 시작해볼까요?", + style= CallTheme.typography.bodyMedium, + color= Gray800, + textAlign=TextAlign.Center + ) + Spacer(modifier=Modifier.height(30.dp)) + PrimaryButton( + modifier=Modifier + .size(width = 280.dp, height = 49.dp) + .trackPressState { + isCallNowPressed = it + }, + text="지금 전화할래", + onClick= { showCallDialog=true }, + containerColor=CallTheme.colors.mainVariant3, + contentColor=if(isCallNowPressed){ + Black + }else{White}, + pressedContainerColor = SubPressed, + ) + Spacer(modifier=Modifier.height(17.dp)) + PrimaryButton( + modifier=Modifier + .size(width = 280.dp, height = 49.dp) + .trackPressState { + isCallLaterPressed = it + }, + text="조금 이따할래", + onClick=onCallLaterClick, + containerColor= Gray200, + contentColor=if(isCallLaterPressed){ + Gray800 }else{ Gray600}, + pressedContainerColor = Gray400, + ) + Spacer(modifier=Modifier.height(62.dp)) + Box( + modifier= Modifier + .fillMaxWidth() + .height(71.dp) + .background( + color = MainVariant2, + shape = RoundedCornerShape(10.dp), + ), + contentAlignment=Alignment.CenterStart, + ){ + Row( + modifier=Modifier + .fillMaxSize() + .padding(start = 6.dp), + verticalAlignment=Alignment.CenterVertically, + ){ + Text(text="\uD83D\uDCA1") + Spacer(modifier=Modifier.width(6.dp)) + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "채팅을 많이 할수록", + style = CallTheme.typography.captionBold.copy( + brush=ChatGradient, + ), + color=Color.Unspecified, + ) + Text( + text = "상대가 당신의 취향과 일상을 기억하고, 실제 연인처럼 대화를 이어가요.", + style = CallTheme.typography.captionBold, + color = Gray600, + ) + } + } + } + } + if (showCallDialog){ + TwoButtonPopup( + label="통화 연결", + title="${firstName}에게 바로\n통화를 연결할까요?", + positiveText = "연결", + negativeText = "취소", + onPositiveClick = { + showCallDialog=false + onCallNowClick() + }, + onNegativeClick = { + showCallDialog=false + }, + onDismissRequest = {}, + ) + } +} + +private fun Modifier.trackPressState( + onPressedChange:(Boolean)->Unit, +):Modifier=pointerInput(Unit){ + awaitEachGesture{ + awaitFirstDown( + requireUnconsumed=false, + pass=PointerEventPass.Initial, + ) + onPressedChange(true) + try{ + do{ + val event=awaitPointerEvent(PointerEventPass.Initial) + }while (event.changes.any{it.pressed}) + }finally { + onPressedChange(false) + } + } +} +@Preview( + name = "Onboarding 6", + showBackground = true, + widthDp = 393, + heightDp = 852, +) +@Composable +private fun Onboarding6ScreenPreview() { + CallFromAiTheme { + Onboarding6Screen( + firstName = "민준", + onCallNowClick = {}, + onCallLaterClick = {}, + ) + } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/res/drawable/onboarding_check.png b/feature/onboarding/impl/src/main/res/drawable/onboarding_check.png index 58ad8d5cc58459dcd2a0ec4594e5fa03ab871b68..40a1644b9a04b08f86c4e3cc2c989955f9ce8e1a 100644 GIT binary patch literal 17985 zcmV(?K-a&CP)Frrn_Uj8Oe2wd*e;#_Dwl%-ySjP+i~b<6YWkq(f9}?YQ(qDB8Z3r z0>!IY-4?O}f&;dAl1aPp!`%-rfs3CxmFEm{$rJwWf30;OaMu^=W z;OH2@%naa$4FKKl1gu!P{qb%0U%P$pLs$OIkH3G{r$7DaWBTzZZ+_`+FJ(Y|@c!)o z_`lb``|X$ftF+zjmxoOL-pS<+{ z`Q5wcpWjPqd#M3x=R@E7;ulXp@67W*GT0iq7fXkreee(N> z11rFs>|<8T`|01^P0!9HcRZW+O@Bs`) z`1WT$v;J2uyzF!BM;09d?G++-e9%0LkVl>uX*)S~jw**aqa z7}e?=VEerxw!l0QP{9)5K>>^cN4D_%MLIxvg5>Y{mtOiQxn`K@rk~R<({mV3dKpYC z8~&@0{m1|FgPX6qstYe7urC&%InXs%|K)!><)XJ;_O;gikG`y#JwjpO0)XR|BdlF6 zuGD~FZz`6;E;C!@TydL}!Bn8Ytd{pR%Z&Q|esBro_e`8Zj<;&TJTTc4b$LG*u*N(HWcCoW+9UfUW^7%{t_}^TiZ21=s>@ONn2J*%)eS5{(=f3WH zt@|H2t(lpOt;M5AC#^;tAJb?Qc|=KFp8zL=&0sQ_DWJL4@jgufHX5i=+-p8|qo5eD z`WeX94}~<5dIIzDIi6<1ge}=O4_5w>Nb-w#G&gPdgrQ@hA@<4`L=a1ZBV*BP*mJR<6`QjRLfh z98y;t(%+&2$l&HPsADW?TvhAjzejm3yI-CF?wWPq?20u8XEMg0oqx{2W^#6HCcEN9 zqmmt;x``%d_P%NK*Uq~RbTY#Wy!|2o+7c1i%Xv=lXb|HTXbz`gb$}@f#&N=>k_ysWH7ZOlD_U6r-TYvO> z|JTiU^Ojd-WWq_uwczA6yaQWBsID<8m=?>7^z;4$EQn(1k3 zuQ@Tm;_zmC?WSwt1+>-|3Q#`bJwN>EYgepTcVqXSt+AcrB%XaLLRN7iRZdv`llY~e z%!x}C0YK9ZQft!~3YK2M5i`Hj7JwA*gZ-1wED*PQqp@B)Dv;Q8OO&uON% z@B7Q;bAva=Z4dR@n@$Y)`qL3QZ67EyucP@SDsvT9*~nK;4>E&g7uOS3+0~Sy*A(Eh zs-tlg=X%O|n{wPd;OvqcO;f~edu%2F-D9!2UW=RNmd$(gs1UXk^Rp$gYo_9!*{vra zY4TVCHcjH;p>}I*FoxUjxP-oU->%IUT#%=93!dlg`LGa6NgT1@%*$@^C1D^eI zgskdBVXnS0@lh*uQNW})P4L;3_EaF~>#h|7qdcQPscv&0M-p*gSI)g1X-{$47<%M$^~SF=!^Io6e#Yt)0W& zQ;&P|IS-T(d42$RzP8p^KlQ00+BrGXyJO3Vtweb6m(M^P9+1JXO=ml4PM4GywW^+q z0}cN!ZnejMhdIp!4NN6O0(2%uvBXvu9zfZwfa5{~sLPHeH|6-)sF#S!oGTFmYIdzI zyj(}IbUt==yLm(zXq6)Ke%bRRAYoz>aM40&jW2FBIN4i!ByK(YtsCA10_O8#T+f$< z;=X(CSv5X0wrh6RBTeVzwXJaKI*nT(IVEU(GWVrv236??SbX$6Qh>Dl(skKhmE@b1 z*Vg3(sJKf~>%nLQObL-PUDQyFI{Ap6IDHZ0qY_`uurfqn~D8BJbgnOk6SM8 z(aKhNQxZW&ZhE}}q5~+*v`^Agz){^}j-w$DzU8M(Zb03glB;NzN(Bgz_xS$GY(1q> zX6yQ`GhJcE*)4l63gE}FW1HKSO>$JS{mfcZuA1{zN}4n-8v`0_Lp*$>X&s$w^?v#6 z|7~F9$S2`Bg8Ezm#mO!Ay<%i?;ErZu-yELxvJS3T+#fH^DuIft?3q>80zmgol&J^3Nc$bhz$8~J z`E~Zf3EoVilBLpuq*o_pj8f$Y_8o5G&iyTV!#P(BoVxlm@EkyWE`Wxk_dk5 z&5`|c_)9PE;L=g8V95CpL7&@06AI#d#Yf6rl47VU?!_YMDUc?lqe*w90tADh@s0F# z%SpjJ1N7&inFU*hL50mqXb`ex#K{MW%03?UNQjbqAGE!7vSiD)$(@N(^1m@zX-Gtg zOMuBzA#2!s4m4ppw)S;;7q7VZf^WgI1oYXGqSm)R^QG~H6PUs5;j2$&Fd4KQFnA!K zkdIzTCm6h(ZW!@N8fpHWEajN1)S;e=16?Ids@;~}rphym zyR|$Ml)&~I3tO04pL~)3L$p__>I?vwR&1biF+HbR(_JA-sT>DuSG1ZT=q;zAZydbw zC!3xvHqYiwt)%|oTh6^;PjBaAO*nmhn_Wj9m|$_JlHcYe1&Qoxjmc^OI$wy2?x-4R zXoTHj?Ut2MqUL(20C)vO87P`{=Cf45qq0pn>c0vP$fG72{Ql!loiC zk-U~^^-(aDN%KK3mst$;7#Rym((!e69Bsb}RIt}nYmSB83WZglUAbRjwdfX0_E}gg zox@TlNbSHaGB}u0yG08CS1#k$m-yJ@z0PT8%;8DP7ucA?)7+kkh30*G@z~4_v)BA! zVQ1xH1+ypDVsXT3m2)a!tD?hJIXBPU_JqBnTB4*VvB-J`ax5eXHB})~f;0y5_eNZ< zS+a>29NSzk7PXCCsZI_QYFw;dfnraS@>EuvIj+V6AVtkN0bI;Q({dG56d{9^BrLuF z&672%Xw7w5aG*nnA&f1Auwq%ed)to(0XG;VMMmJT8|l8?GlV<|?YA_(-k*-A$>+0K}&u~j8Cd>(e0 zW_{5epT}I;m%x3ki2>B_POa#Ul}|Q7o+~T9pMV&Y9vQLXBk8GN9)O?|BlE~kDx41u zaykP`QtG#A8PMI^Vk`OOpG-Wq`s~Xu-v>`a%zrw7a#etKP95#tbjyP9s#5|Ux56np znambT4vB0@jSK7GtW}ACwUT%RB`Om%J+`U90WT<{Dm?k+U>4)CRj($3syhNiw#;CN zD{!b5W!f+Lg^*8B!fJ3x-KfV-gE^L1iuHRmhVmp);IZVTE^f4B-z+xp`xB^KjaoDc zxOxSH-E7^~bT*zog{Q1Z`OVL7X?Z#>w>fnG<9{B1@RO0SbTq)amEs<_*h~^DdnIOy z70zj8zgNhSyJI6qy;3cSm52}zQr;%mJ+$k z=E1JOEbb6$->u?<<&)|@wRMz&O$5zfQS;IKI`0Rr z8*#5z+KPSpEV}YjKw~%R0yy=nI$EGM>Cdgv%mhHoT~KP5+Wb{yIlY=lMNd{sK_xl{ zAiDLDh(bXfCSuXtgML0sMH)-xzbA;BX8o47pwo`}@BW!0H_2z_cznWf5Dpxsfrif8 zbi&3@Ty@>IyHMW#Guu@jiA^L8_HiDV zjjKNrCz}#4t#iQKVQf6EdU^t7(4CMp0*ruZwMmVMIVZ$uh6XI#Q6;RK>u$(XFZ6ha z!d6L5oJ~!PMB$Uy2|WavDe?rc&p;^0w9zFWVR6+8GNx1pvg;kru{7oP%g1@2-u(|Y z1Djs46Hi=wLb>pt-JZ(j;-g=?{=!t(JQ)`*z_4MJC^=QE>~o{NDm7`ep4lt;X#Sh1 zK;$|snQJs*-eRApAH`6u&j)$%^bD=cMHQG08ICGIG@7T%0nCz(n2JGQADCsvLgj{0 zCqo$^tKfj9DPoxdxWrp=tqzu@cln1P`K=kQ^9A!zxwwC98t}-`B;+rJ_Sk4E-gU>i zd%yCP3*jjY@yY%mh3YN)j>Nm}8i7}x9AL?)w=70uovP1z)0P*eqsKGU6hss|Q}=k`A^L9Rt!x41g0Ye;cW#s#SrC1EmyZ zL#Cgt?zdPU$!yBsg9#Pr?%#-$e@^{AbLqLvmaLQ#>&^93TAHkM(Fo98cQ>td8)k9i zDI@SCR{CTXN{6@X{H^BJ+eUHWaDZitz+|=zPB5{61%mX|Rk5Jk>#wGCHLo;*-C}O9 zYKm5iB^MJKsC#CO-1NJ8=<5TLrfEyuRUptoa&JkNSCSN>DXL{h8UJcT2+IJM@>*2= zKvOFaFpEo-A?C$VL9A2$REC&BjRIBVimLOrR4qzMHTTV!VAqd+ z{BNEFw}s~k6tlmHbXRD)buQ!oOA z77Jy9*q6jps@|_|IG78M?g2n23~Jqu)v!EX(^EkEQyRKqJ%o{wxO&N=j|23Fzl^ut-btn(aN&@P>QWt<^R^JEcqEUMxB6mC?DYlECb2FA{LTBCBG3VlheC7z`TAnFPmJnDD<}8JGk?QH@p{~#2V|3 zkG6Dp#h=rHVGcs9TqxK4yL1i(`E~N^UHlv}y`^ zb;N)y6Ecvsp2*;s`*eL4%gK|ur^_)q{9T>&c)?e*xt z^J&|}K&lXDRR$~-VwY^zX`tutDX*uQr1c)ym(rMZ&>C3~*NiXuBs_tY{)~k_`1NZp z>fLkS7!Gv;gOw@)(#&Pn*C>M(_sc(L_S>z50=CAv5-UxUHIYD>vIz6ya=h2bMFcA8 zhYR&hRk%hcIl@n(Tbv)yCIi^9v`%UMQe7?sqE>H}P7#HQp6jGZ-%d)1#BCec;6Ray z3C5gh9&xfsy>g@rZdFU+JTxV0#1u8L+C$q;v&HprRx^DK-6+s z?6W*0OwuWVZmG6j#!>`VdZ;2b8V`k(HH9?k#0t!a#l*>m9g1x^aC5VOv(w_T z^En0^;Zh{vrHE!I!C)7nWHFx=B4y-4oRmoB+;lq0f@5R(y(cp25A z-15}rIoNCnsz}9>$APk4xtiz$C#(~pPB980A;-Wst+cpor*SS;mdMoa}Q*+*Bm#h)ixb9h0UK z>s_`iiR_2y0~cQU2ldtT1ys6q_hh{Pz6G#g2w`RxXvso=HRFlF_FM$->%^;43~M{F z{*A5rDHMt|lUSYwuJw58`4l)O}QRl5ahD^gpq;}L0p zkfU0@vyubF3-pav1_50dfXNAfw_Xf!-NHKJQ4 zTAPZE2Lqp5KSgMOfmB`_Nti5M2DoG#=#E<`tluz!C%$|M%(GDZ#2;>cIXwL6NFqVV z1?UW1R))GgU%H3BNXSxE4s;M6 zdwB7cAO8=_Z6pj(de<9&{eO>#4^!@QaqHuiRT?kr-r2b-pKQdx_gQRXF)|iLOI7@F zl4PYZ1fbF?LKVsJ9wpG3t1j6|N3M@ruMY~!?B0RA?W1%fU=b&5_K=Abm3!kqHU1W} zrn*MTDa3o<4KTXU$(>1t{MHOm7Fo_apVc1Mge$d3I_gnV8bzapg`}ZQUUV|TUzp32 zlSkQ7o5X(4-E`LP*;#uGT|K_`9eEv!YmWJwsz1CGp%M;k8H2T(2!J5Ou_{=5Y64JF zkiIYTo|RjpQp%Foqrhk2_yA`&j_TgPNrxyc<#(I5;+`wF7D`OyEf~n=0kV~tWsvs= zu>~L43-IpCfD)Me4y^J}04-)~r6%7;X~|-h4XLjAwMq0;r=}JrQ&gV0o={3{<)=mWcz_bA;7I zv6HmLE&zQJAtpDA?|n~JY^urE|5f^r)EZkI{eyh2g9|cYLFGo?-@w&LCRUB^u4OFd zB#jv!$=!n(W+#vT(1$)WY=Gjgo_6*{w0A#OQ@~hhK2#z`d~5~M<-Dfx0yf{vAQo#h zZPiwW8Bj>I+wJd45UH+MxvMy_6rlNHi5%=`?hC7l;p&V|X0z%l|Fj&?x)fx<$Y+Hp zG*pq3Di$9~dg{9`XD~&B=696&Z zH#6uIOJgEQaqnJu%V`_VF+gd}!twV-K4=h^YGtfrbFK_w)fePl2in}_lAgLrZ@!|+ zS|g|h>QzR|DJ!XuOmHtV+^oyH-E{&4EQ4k-$crp$CgNSG3?PuFIOxk0?4z`6Ag@co z%6c`0fJ}hionq$LA}7T1d05W1au_ygFr^qUd05=6o&i>IGX^c8nWU&dmKGMfo>;Ce zf8{tjlicp&B|HvizjD)W8lbT1gqNMotBma}>b@A5V_kKdYNb)T&ZAi;_|IjrEhzDc z;I+X<(-0NZGe9wxpN&{Jc~F*W>oEoVM&cOHRK1s zNSh40T8yC5mGk7tjB|go%=aYATs$Tr%>1Y;QU;(gZN0WmBAC~H6X3`Z$J|-4pI~vd zSWP@5Zr)%u zQWW&=Isi~NS$FQ0{8A@J4YE~4qRvLD;+}mM1C-JdkjFc5TjnYW`l=Upd{tqL>e^C6 z@Akuz(m)jfb1rKAv&JrQ-<||!vdkp4krFHcuyUaKn(6~G(HP8c0c_tU_gB!#;;!HG zPw=>!&qu0rR6tfHEIKei&2$k&-{lJ!N)@rad&25YXJr^TeC!N*r&GZahaM4@N z4I>S$&+eAr)3{dxPaCqeX6^FuRH>%dZq!;ltb312X)M^j5xj>;T2xWo#i5CG{GP7<8_0KMi^z;j;#lvgt6KhAU^{PbR+8@D8FbaAQ3gn5I9bexQOiBa@@4p9V;YiBo(eJ0gRhp{0 zf^|Gpc~7oWf7fdQll&OP27HaGy_eKdGcg)m$nJO5iR+FJ(-TL=p%Wy~q7snm+I=>n z3>a}5Qx&Am!&Pon$a1XMsj|}ix1*B)zwriu*S}Jz1>d4HftfG(+%o{KxHyrKgH=j^ zI=M8SLs>zcXK}2~%|~B4x2hS5#f{m!&2ms;G#)w#cU6pP5VszReZ?PXjiB$}{nu?}R3a z-Ez+%sF4EJq+j;MXkQY;J3%i)fr1JssI1mL8&m@} zh6wY>jkb^^5&@7bla`G$A#*F{&rqff0OW#4u}u1Yvdp`0=ZXu5w|tJ~64lj83Z$>K z^gLA=vRGe~si>@8`ia2Zb1Z}{Su1%J#thC3C{L8wP7{)XOct%eYwxLd->IqDgs~W8 z?ngPLDlEEZFtEX`ng5dG3iU7QQPT%s8eqgK3m7?Vc>g7-!f{yLek{iXC;*vDrP^H* z)OFM%4XVZLOKkR%cL0r#O9cr__X6sa5KAo{fzK^jX1xAdfGu|ZPogb*Rg0E_`bcPIg_gSbOCi>*C6Dw?8f9E#VhVOlpt|0`jo11IldxS!{5Oq@? z8P(D}S4VRIN-j`5{i6Fqg^1v6$;D9jZZ9VDtD43_`r&TEIJRONH%G z0n>owMZ4+qTYBxB3$-hwv z5&+ttDj`t^nV<|>{&+7vl?CV4_CZQuYAj6wg3$#4-?>TdPN{#S+Vy&jGRY65Ot&QI zsK5EP*(V4nH9ItS397y*eL8di=O!}-Rw=* zr_|~e?owcDZ?~L?F=^q3uTYUQRk>_pnCDPc$dxKfW%$}6)S`!Z7F2Xsl}kh z>@09BT>|ml7biw~P+WH`dTIgHCS~Pej0@xoKb*k4IHg6aR%p{!#w|xnZ56cv1W?^v zCqcgL%#UsWy!B?*b+ON_1|D$k5M)AB++sdfbdMpN0@NbWBtTHIWXxL$6{?4k{v(Z* z0Q0&7(K7|TK}a%W3|(I;ptGjlJiRl}G=H&za@jlfxml`~0+{P!2=cPHZ~h<+&K9Bl z)kaV*ga;EXt_sP*#6;n}iTxftz!#2Hn`V6sfMU5eGr6Jbt;x#QtO1caD@&$w=S*LD zZ8ro-ZWez0Lx7*$%xu=dR6t2%CE9%w%o;UK!Ft+xahuA_F`>jm1i1yGNu~-!XE zYjqRFY?L2ACXTeS8A4xLHy?R z0Jo?7E#FP0vQ(djl}lzOoi97b)MY8|y*tIfg9n97P$3D?Ns}f;);E+VK(b6)vs$|U zEl%r&$YFHi*%ZkWD7)DV=FK;9VVf_%Z81}UWCn{IGpUY!2(H~?m5aOZVT_i4R<>Ie z62RRx3Qs_b!t!&d*Quh8Dm?Nwb{+)^KKc76<3=jJ_eVqPuw4A+0Q0f z28o%5JzT&91GTnbuNlzxH*Y6ZY^pfwj2@LpQPi9lx}h^RdWdyXmCMlJRv+z@UnzEJ z(bw}ZS`MI-!k`Y0w0e2HAW5uw;w&Er{A$YQw%o(C+hQNp{gh~)-rSOi{Ml0DS19czW&H}eo{Q#cA-eUFr7eEF)JLA=);Cl@_t{u!-CWh`!O<9xgx$M$K5QL& zj82egWd|icwpxJkZ9AAwMF|Q02{`F4)>+_jlrLPKDwqjMyz1*fcix=}!}`nS;Rg;8apP`Q4wG_^2w$p9wSLn&w@_@tqtnt)qTz2#qf(>x{v zqhp6dGthom9zy}esQTTrip-$|W{WpePMqxS5Kv+fS8ysy-ua-(S>gTTE8A*cLuzrg8=Nl2IgU7 z1uTQPCFPjcUjwvgOf0LdQeI0Yid$T^%60)Uv@|qQht*gDCpiYa>3fggbX)MgQ!( zKY_Vhb=j7$$)L)a$wWvIJKs6$AFc+xcdMo}dTg@uC{e&Zlk?6b*@YF$f!_HJ$uTEW zY3>cLOGSVUd@o)jR5E@DMFK?S3L5V-n0F`j=eqAECO#%9NLwZzRSFQcg<;CN7|~b7 zz}5QQ`U2D{k6UxA21!MH4JBd|%8)hI7ClYdCj^DH8FvtZSi%5tqx;CBJ9CM=jrXD;3Kb;Tn8oIPE> z{-h;u-@fbCux0O#$6)m;d9wy}RNpNF0M$W2-GL4SDA}>TB8_il9fxYXvA9Y*HXoSD zLYtW?T2*CrQgFWbkGyIt*ZI7UH)9#8N^PK;NF?13Yt{ms@d{b(j1Hm1yAq7`Rdv1D z?dQVuH@}wH>|)uS(-Np$tpcP4=#ss~^1a?iiI2zB{BkQeaK0SFElrJpOK$|lO%s$V z5>*yY9=`bVnq;B98JHbdaEF}EEjxD80}rmkv0+%4B1MU*GASl(Y_g*Rm9Rc>=2RgU!go;`s7;!=Q9PvP9rCYQCA*lex*g`Q7U zzXX}tMWaLJLBG~#6tX;ji#Qqm`r$YZ_U}QpY zJk6lyeb=l?G4;wc@Y~XPwcQroT{1}E7TR|=P5UgWBCfitJaOQWZ9rEi*S>hETxUlg z&d$UTTs-jB5l{M6p459Kc+ObIO1?bO+!Ndy9Wa#ysb;|*9;#!^n?z*5d)RAnlUVFc z(Vp!GZnU`uH|@OVyCLUv`6;%8ZCCVpaM4_C;{z1XS|!R!cr8=`@~hB_fZdYD+bHye zsjdflV^vGE$~9^c@p!0IW9l?5YU(Z9x&HJ zHSUquGeN|ImNH7V*ihw$2;KzF>Wm#2``oOkJe8&ThUI(|Jn91+p-fSz`iMeFzYNU0 zYbHdkh!5QP(5D^H-}}f%j==G2CUCY#SzFDsloM&C)Rty2v{<_M+>5>gMkPg+hU#_SvcBGi3Q zQmPD9I+!PN8{E|AW;t!U@<$Kc08m0Z96Gf7pW@WC45~h-OS;!87fjSQ!wgs>Ayk#F zWI09j>V>s&Kt*G`l|%;&-3lb0@+@pi?G&^7{p{ZXKKzJGxBwN|q0)(0(H_v)CGIv^ z_wJnn=J*QvTn4UH3e@eOOY*VbBH5d0I_DOnR%lji?y_R>o^j~}u>4XwKA>+bgOe+B zza~@eYTdjO zp_YRAg}>(e&+;z$y4b~|v)Euu2~Fv#?!90GYg{0M9t=za*KZLA{(2D-0(dV`D1~kQ z8PuBVP3Mw0BCVxT)YK_Y+aR~;OYw8}1N;8Y-e!Q(8{hw)yKv*F(=^>BZhq!i7Fe}m ziO$s#^c>adeLAH6o$7dCEYEtAu$;V8n7&{S^4Y00(a4)FJX3%GJMzm^G5G}*0f@d&{?9!%=wgGYDA;!Py(`(P`0&O{M> zk`!~npNW+bOZ^yY3s=Cw?8QY29naPfkhX5Jo;}jWZ(jS;zW^w6&bK2p^|ky&T2RKv zib*0#xtTcE23eBp1SpwD#Vm~hqq(uPR3 z0maXJ$5f7WEh&{POL6dvUjTf3mrS;N1FN1sJ3AkvlO|!(cH#4Xon*mf@?yTJ zuvvUtt}m)$l}|T#nowM))Hk7=n<_M3e6~@Y0mWm6nB&s(nN}MgU&{2IfVoiRB=wChb-x9R{wos$~M&^?;j85n8Ix za7hpPC%lF#8H=EdDZghJU}Q~F@eZb+(**7&elgO6k9_L3!OfdD&s9U-x8ZG{`O>@3 zoE`WAoU#URaz-q4Sf6a41@9~ic4zel$4z+%IY?ZI){}!u!BJ8GUxmyk9z~3=m(J-d zBITNV$nS-*7ntn!DMPMe+@z+-(QdX{#JlEwosX z0TZCVc3Fb~%6?-3Li5hPeZb-%6l;W^o!~A$K;>Vxfjyv}|4<4Utzih=L+SNl=pCCR zJpScB>?}I-b$tNN1C%KR{rJ(jxOYd3?^Km+Az1Z&7}(gb>Sh3Qz}QC?O+@f4v7M}Q zQPQ-J?UtqZy`iDll;TrX27O*g3s8(V=FNEqJn>V2qrU>RK|#i3Fsb2HO^Y!E2~L4uD0av zhukh{u~983iv(m0xmwyMqB}@xW-d-G%W}eq)*_-`#&X^I8U-(a-z&i^8j9-`fMfzo zmyg9#@K8nJXNe)FB|%fK&N7+I&2+MdbgjJ^!Zk17n!vmtp1>mKZ}|Ry|3`n^-n1z{ zbcZr%Y?XOtj||D9hVxyn^K(V=?N>W8Xgolss({9upl%nia`068Zfsculz37sStmwC z3asM6>^tYEot6`woLU8D<899`IgcKwmMNGvYt3G)+9)~mHW9U$psrsnQqmCsXQROe zTqlBczANL$P+J`ppj%KTyDsWAwMmNNtv+NhS5_7t4>+@5T}8Ui)-?X~ySM(P?nQmz z!4q%z=yz}Ue^>81`fppO9EXq;Bp6=;kX>(HN;s2Z>s0*lL5N!S6~0{+M7-Tnwp7}(iO<7xAR*U4g)S+kV;PJ`^XTx zVtz1qr^KcbJ2XNd^jWA|F#k%WbKEV~W_q+N!QUsHb&@u5%F|XwOQkzr{Mu5b%kF^b zt(OXtf#G3-Jbx^e6)PMl_!%U>RU6MN2g?f#c~`4C1-k}4cTY>o_*Tr}4& z>nbn;Wol2T1~1f?TF$4>g__0Yzqd<*&ucP$A8_HGEMB(6<>sG*r9>(Z+UhvI7QJv% zx9iQ|Ag}4o>G-&`=%h5RYm+##1-Q8h+i`IDX!yt3F^`3^xGN#FBbI0jVBu@oU-Rm~^AAgr|8dSQ=HjG9T3Pm2EaH~OXV zkSv$1+?ThDsAbkAFR5N&x%vcHu=kr=S2qc@ZVQ#7q`azh94Pn}ZwdN5l6x86jod25 zd#SrdRp$nlCUxu8K>KczR+DBRi6#5+P;1S9-2AbR<@=#GPX-#o)7WMo*?(2M;YaTp zIQ=BVi7DV`h^|X|>QsuWS^J$hnuOqFz4_hUf@7aI&#M%o1U@MvKqc4NdAfT7 zOX*}nxyva(XHeH%n8;-s#{)M3&b70C)3i@sJB#7$5k1@ZWLUM~=Wp%76UHy2Teo+c zy}JXBEo{;5{V7YWZ*}#xFiI{W=QGfSDxa*_Utgk{{s1Ryrt_nw6V& ztABwkLLmWANJLpDBO`40?5pFS-S)+0LD1B|m}vq!2D4JBN0Jh+k{4?F!N-gae5pqQ z)F~J0GWl}TNE_3#E>=dmOfE%Fj!n=b{9Kv52Y({B3g{kd zaA>ZzZ}#}JR;)iiK(oM%4e0fYu*9fiY@kH&2uo~C8m_Dn6*ws7#)`%Yi7|Ii)11?{Fa*luwn){-$PUFC z;#Qr32k{_OHILUNufET;--CUqStB^Olow^?Cqv|%GJ(ien!#T9GA@nLq1$EU`Sb+M z%^tv+@t6JW*!XMz7d`UNjhprMYysu>?!E4&iXYumLGKM@dNLJ@bZynhS~W^W ztc+YiiOE8~LZ7K}_kWO-1j3+$i5b8gJBN=Ku#eoAPBV?o)IRD?9)`h<7kv-gtKS3? zH=nkvbx`2hZycER|M%-JS=O|_(j-ewLb=@NJKyOfv)yc&*+OTIX)I0bEkCWcdLV&S zBCO}tCU`xNVj)#|D$I+$l~oqOz+7Xj00VHq&6Q(SST_Qt?$qL>$J_e-{OA4&DB_8_ zTxGBfWG+y*N5nc?3DBYObV`XFO>KwJJC?lJ0h*gS1Ouo3(khTVgRz(kaN@Au9B^Ec2-(fvg_3c$uN-i2+2?#5x|9QE`p=> zqk4Qbu|ImQXeBIBYY6JqVe=i7*EtG|;RT64_+^>bk)RG5cAceP@f3CP@? z*UCUHOde~Yd{4k)XN;Hr(d=$kgK%mOb&u|a!PCzF35HYivsa!qn0ji@d1FwI+;aQt zMvqM0&Y_#x^qJUoEOmmTo*-!FfLmoO`{{^SJoU-pOt#w3`*!s`AZ0tZ%4~@V#0($|EE2by zznUbS_L5YZQO*5AY zl#5RJ>qX<|{uVqBVEQFJ4;!p^J}iO6q_|pnJ8iRHqh7XX2Ye2wyLE9IOq;is3wZaliqY~GN7T0 z1}1+n*lKg;Eg@TGnzRgNcCn+$A{Xn|7{^Tpyn7%WB#p(yLy1!y?7`qntGD#*f30BV z6L`*G+9^KY8?!SydgR9Lx376^d)11>fYWQ7Gin^n4`R#YsHs>lQCaCs^OAg!-k-7o z83yM0G7GCTqe>n6l6%Q5O_?eJzjgAN=>hxOu z`ns09G>a@y0`ywKd3pvNN*Il#4%EGXS1{P~|@co8a z4tRjR5?Kur;}qseby}mW1p_X3>9Pcr?_^B2J;wD+_6FU35K_#H6A$N@8GFYLQRl=p zyKnv5hXxXu_rUWECcp~=Xx_L&vvBq5zxmkpKUgww`e~2FM<2^Y>c}1UxunJb>B_Q> zuA2#_2w_?AW+{Y`k5E}|1>XF2k$Q11^%cHNzNA&C|Ja&_I#rHuG#3p*?zx6BQ}U zbFK8Vg>V2*VW{u1jhs|Y8 zdw=45GqIUE5IP%Qb?}GZxqajL7ruY5jR(9y+b;spyzvlSaKQzwYrgb_?}ne=c0raE zQhF0GH$b&@EocH()~{p(*Yzwe9w>-VP3}H{zT}AD?pWR@38gVzi_=iOze-W6GbXiS z386t;E?Z?1PC6qgY%G?znPX(0j5)8IJ)Ca@MZMXh&^c}yPE3q_VDXxBJ_V&>^1NH+ z{DJ;OYrEt!oCKUqdF9k@nmxLoaE!3G?6@y~#gg8%bnxdBnLPwy zd=zI}tFHdXFZ}e1_kHk#8ORqjPCnK4Vgd^D&Vj!47k{?myjQ<^^QyH=KhQk*5K?D| zLMKbZU17wzp?gr=vBa!=pNPiYFhK*}kpQ}O#&cLwc`;}XYOXu@(tiIPTW+${bos%n za!*=*fDke1p(BVVoj$qa&K-Yp?bY}E|NnCH=7jW%2c&H;1wiY~2jp9y{P^qNbooVp z+CH%R+-7GIvKK6%RLwzqSPjS5TuAQ>%B^nH2)SNLvEO}~PV9b|%9Upe&ClVqCqiY* z%OKi42H6rRTV*o#V~Fcd?#&(!-?-`42ma`dZ~a#$3?RJ7K-%_F1JpJj`LyBVH+}aF z3lBHU(@OyTLs-zQ=H5tsad&MmFA~TbofS1*(w_Wm|xcCrsaF(<>$#9 zOG3c$Yx8ciG`#GgJrC}@=FU6rzxt;8@4s>L=1(81&IRBl24cPaa{zSy)(3EYkodlb z9$IqZijlP!UG%0iruXf9+1SF)38!q_v=WXT9;Z0BFcGO?4xy>OhV9OD7#TgVb?bxs zCJr9iKD=`EwkyBz)vd>GJZ<~cfB((hU%U0zV^8&YFZJzz03^{P)u8TSCjbBd07*qo IM6N<$f-A2qP5=M^ literal 16956 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%0A?FQ{O`9G(&eNbuLSVO23Z-hXm$j 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 Date: Tue, 28 Jul 2026 02:06:59 +0900 Subject: [PATCH 07/17] fix: preserve onboarding AppScreen changes --- .../java/kr/co/call/callfromai/AppScreen.kt | 82 +++++++++++------- .../kr/co/call/impl/component/MemberChoice.kt | 2 +- .../co/call/impl/component/ProfileChoice.kt | 4 +- .../kr/co/call/impl/component/TempControl.kt | 2 +- .../call/impl/entry/OnboardingEntryBuilder.kt | 81 ++++++++++-------- .../co/call/impl/screen/Onboarding5Screen.kt | 3 +- .../co/call/impl/screen/Onboarding6Screen.kt | 10 +-- .../call/impl/viewmodel/OnboardingUiState.kt | 34 ++++++++ .../impl/viewmodel/OnboardingViewModel.kt | 84 +++++++++++++++++++ 9 files changed, 228 insertions(+), 74 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index f8122f8a..cf5be678 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -3,9 +3,7 @@ package kr.co.call.callfromai import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing @@ -14,20 +12,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay -import kr.co.call.api.CallRecordNavKey import kr.co.call.api.ChatRoomNavKey import kr.co.call.api.ChattingNavKey import kr.co.call.api.FaqNavKey @@ -50,6 +44,7 @@ import kr.co.call.impl.entry.loginEntry import kr.co.call.impl.entry.myPageEntry import kr.co.call.impl.entry.onboardingEntry import kr.co.call.callfromai.util.toMainTab +import kr.co.call.impl.viewmodel.OnboardingViewModel /** * 애플리케이션 화면 내비게이션의 메인 진입점입니다. @@ -90,49 +85,74 @@ fun AppScreen(modifier: Modifier = Modifier) { val bottomBarPadding = remember(bottomBarHeightPx) { with(density) { bottomBarHeightPx.toDp() } } - //온보딩2에서 입력한 이름을 보관 - var aiFirstName by rememberSaveable { - mutableStateOf("") - } + + val onboardingViewModel: OnboardingViewModel = hiltViewModel() Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { if (showBottomBar) { - MainBottomBar( - currentTab = currentTab, - onTabSelected = appNavigator::navigateToTab, - modifier = Modifier - .onSizeChanged { bottomBarHeightPx = it.height }, - ) + MainBottomBar( + currentTab = currentTab, + onTabSelected = appNavigator::navigateToTab, + modifier = Modifier + .onSizeChanged { bottomBarHeightPx = it.height }, + ) } }, - contentWindowInsets = WindowInsets.safeDrawing - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - .exclude(WindowInsets.ime), + contentWindowInsets = WindowInsets.safeDrawing.only( + WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal + ), - ) { - padding -> + ) { + padding -> CompositionLocalProvider( LocalBottomBarPadding provides if (showBottomBar) bottomBarPadding else 0.dp, ) { NavDisplay( backStack = backStack, modifier = Modifier.fillMaxSize().padding(padding), - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ), entryProvider = entryProvider { loginEntry() - onboardingEntry() - homeEntry( - navigateToCallRecord = { callId -> - appNavigator.navigate(CallRecordNavKey(callId = callId)) + onboardingEntry( + onboardingViewModel=onboardingViewModel, + onProfileClick = { + // 사진 선택 기능은 나중에 구현 + }, + onOnboarding1Next= { + appNavigator.navigate(Onboarding2NavKey) }, - onCallRecordBack = { + onBackFromOnboarding2 = { appNavigator.popBackStack() }, + onOnboarding2Next = { + appNavigator.navigate(Onboarding3NavKey) + }, + onBackFromOnboarding3 = { + appNavigator.popBackStack() + }, + onOnboarding3Next = { + appNavigator.navigate(Onboarding4NavKey) + }, + onBackFromOnboarding4 = { + appNavigator.popBackStack() + }, + onOnboarding4Next = { + appNavigator.navigate(Onboarding5NavKey) + }, + onBackFromOnboarding5 = { + appNavigator.popBackStack() + }, + onOnboarding5Next = { + appNavigator.navigate(Onboarding6NavKey) + }, + onOnboarding6CallNow = { + // TODO: 실제 통화 화면으로 이동 + }, + onOnboarding6CallLater = { + appNavigator.navigate(HomeNavKey) + }, ) + homeEntry() chattingEntry( navigateToChatRoom = { roomId -> appNavigator.navigate(ChatRoomNavKey(roomId = roomId)) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt index 66497f40..65eb63fa 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -46,13 +46,13 @@ import kr.co.call.designsystem.theme.SubRed @Composable fun MemberChoice( - modifier: Modifier=Modifier, label: String, selectedOption: String, //드롭다운이 보여줄 값 placeholder: String, options:List, //선택 가능한 목록 onOptionSelected: (String)->Unit,//선택됐을 때 실행할 동작 required: Boolean=false, + modifier: Modifier=Modifier, ){ var expanded by rememberSaveable { mutableStateOf(false) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt index c326853d..ee53cdcc 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt @@ -27,10 +27,10 @@ import androidx.compose.runtime.getValue @Composable fun ProfileChoice( - modifier: Modifier, imageUrl: String?, onClick: ()->Unit, - size: Dp =99.dp + size: Dp =99.dp, + modifier: Modifier, ){ val interactionSource=remember{MutableInteractionSource()} val isPressed by interactionSource.collectIsPressedAsState() diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt index 7dbb275a..84162a49 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TempControl.kt @@ -53,8 +53,8 @@ import kotlin.math.roundToInt @Composable fun TempControl ( - modifier: Modifier= Modifier, onTemperatureChange: (Int)->Unit, + modifier: Modifier= Modifier, ) { var temperature by rememberSaveable { mutableIntStateOf(50) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index 6fb716e6..c54e5224 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -1,5 +1,6 @@ package kr.co.call.impl.entry +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import kr.co.call.api.Onboarding1NavKey @@ -13,48 +14,38 @@ import kr.co.call.impl.screen.Onboarding2Screen import kr.co.call.impl.screen.Onboarding3Screen import kr.co.call.impl.screen.Onboarding4Screen import kr.co.call.impl.screen.Onboarding5Screen -import kr.co.call.impl.screen.Relationship -import kr.co.call.impl.screen.SpeechStyle -import java.time.LocalDate -import kr.co.call.impl.component.PreferTime -import kr.co.call.impl.component.Trait import kr.co.call.impl.screen.Onboarding6Screen +import kr.co.call.impl.viewmodel.OnboardingViewModel +import androidx.compose.runtime.getValue fun EntryProviderScope.onboardingEntry( + onboardingViewModel: OnboardingViewModel, onProfileClick: () -> Unit, - onOnboarding1Next: ( - lastName: String, - firstName: String, - birthday: LocalDate, - job: String, - mbti: String, - ) -> Unit, + onOnboarding1Next: () -> Unit, onBackFromOnboarding2: () -> Unit, - onOnboarding2Next: ( - age: String, - lastName: String, - firstName: String, - job: String, - mbti: String, - ) -> Unit, + onOnboarding2Next: () -> Unit, onBackFromOnboarding3: () -> Unit, - onOnboarding3Next: ( - speechStyle: SpeechStyle, - relationship: Relationship, - temperature: Int, - ) -> Unit, + onOnboarding3Next: () -> Unit, onBackFromOnboarding4: () -> Unit, - onOnboarding4Next: (List) -> Unit, + onOnboarding4Next: () -> Unit, onBackFromOnboarding5: () -> Unit, - onOnboarding5Next: (PreferTime) -> Unit, - onboarding6FirstName: String, + onOnboarding5Next: () -> Unit, onOnboarding6CallNow: () -> Unit, onOnboarding6CallLater: () -> Unit, ) { entry { Onboarding1Screen( onProfileClick = onProfileClick, - onNextClick = onOnboarding1Next, + onNextClick = { lastName, firstName, birthday, job, mbti -> + onboardingViewModel.updateUserProfile( + lastName = lastName, + firstName = firstName, + birthday = birthday, + job = job, + mbti = mbti, + ) + onOnboarding1Next() + }, ) } @@ -62,34 +53,58 @@ fun EntryProviderScope.onboardingEntry( Onboarding2Screen( onBackClick = onBackFromOnboarding2, onProfileClick = onProfileClick, - onNextClick = onOnboarding2Next, + onNextClick ={ age, lastName, firstName, job, mbti -> + onboardingViewModel.updateAiProfile( + age = age, + lastName = lastName, + firstName = firstName, + job = job, + mbti = mbti, + ) + + onOnboarding2Next() + }, ) } entry { Onboarding3Screen( onBackClick = onBackFromOnboarding3, - onNextClick = onOnboarding3Next, + onNextClick = { speechStyle, relationship, temperature -> + onboardingViewModel.updateConversationStyle( + speechStyle = speechStyle, + relationship = relationship, + temperature = temperature, + ) + onOnboarding3Next() + }, ) } entry { Onboarding4Screen( onBackClick = onBackFromOnboarding4, - onNextClick = onOnboarding4Next, + onNextClick = { traits -> + onboardingViewModel.updateTraits(traits) + onOnboarding4Next() + }, ) } entry { Onboarding5Screen( onBackClick = onBackFromOnboarding5, - onNextClick = onOnboarding5Next, + onNextClick = { preferTime -> + onboardingViewModel.updatePreferTime(preferTime) + onOnboarding5Next() + }, ) } entry { + val uiState by onboardingViewModel.uiState.collectAsStateWithLifecycle() Onboarding6Screen( - firstName = onboarding6FirstName, + firstName = uiState.aiFirstName, onCallNowClick = onOnboarding6CallNow, onCallLaterClick = onOnboarding6CallLater, ) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt index 04adfadc..c682ccf2 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -33,7 +33,7 @@ fun Onboarding5Screen( modifier: Modifier = Modifier, ){ var selectedPreferTime by rememberSaveable { - mutableStateOf(null) + mutableStateOf(initialPreferTime) } Column( modifier = modifier @@ -90,6 +90,7 @@ fun Onboarding5Screen( bottom = 18.dp, ), text = "다음", + enabled=selectedPreferTime != null, onClick = { selectedPreferTime?.let(onNextClick) }, ) } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt index 80e7af7e..4fab5bb9 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt @@ -58,7 +58,7 @@ fun Onboarding6Screen( ){ var isCallNowPressed by remember {mutableStateOf(false)} var isCallLaterPressed by remember {mutableStateOf(false)} - var showCallDialog by rememberSaveable {mutableStateOf(false)} + var isCallDialogVisible by rememberSaveable {mutableStateOf(false)} Column( modifier=modifier @@ -94,7 +94,7 @@ fun Onboarding6Screen( isCallNowPressed = it }, text="지금 전화할래", - onClick= { showCallDialog=true }, + onClick= { isCallDialogVisible=true }, containerColor=CallTheme.colors.mainVariant3, contentColor=if(isCallNowPressed){ Black @@ -156,18 +156,18 @@ fun Onboarding6Screen( } } } - if (showCallDialog){ + if (isCallDialogVisible){ TwoButtonPopup( label="통화 연결", title="${firstName}에게 바로\n통화를 연결할까요?", positiveText = "연결", negativeText = "취소", onPositiveClick = { - showCallDialog=false + isCallDialogVisible=false onCallNowClick() }, onNegativeClick = { - showCallDialog=false + isCallDialogVisible=false }, onDismissRequest = {}, ) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt new file mode 100644 index 00000000..b1f07362 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -0,0 +1,34 @@ +package kr.co.call.impl.viewmodel + +import kr.co.call.impl.component.PreferTime +import kr.co.call.impl.component.Trait +import kr.co.call.impl.screen.Relationship +import kr.co.call.impl.screen.SpeechStyle +import java.time.LocalDate + +data class OnboardingUiState( + //사용자 정보 + val userLastName: String = "", + val userFirstName: String = "", + val userBirthday: LocalDate = LocalDate.of(2000, 1, 1), + val userJob: String = "", + val userMbti: String = "", + + //이상형 정보 + val aiFirstName: String = "", + val aiLastName: String = "", + val aiAge: String = "", + val aiJob: String = "", + val aiMbti: String = "", + + //3단계 + val speechStyle: SpeechStyle? = null, + val relationship: Relationship? = null, + val temperature: Int = 50, + + //4단계 + val traits: List = emptyList(), + + //5단계 + val preferTime: PreferTime? = null, +) \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt new file mode 100644 index 00000000..75853d45 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -0,0 +1,84 @@ +package kr.co.call.impl.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kr.co.call.impl.component.PreferTime +import kr.co.call.impl.component.Trait +import kr.co.call.impl.screen.Relationship +import kr.co.call.impl.screen.SpeechStyle +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class OnboardingViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(OnboardingUiState()) + val uiState = _uiState.asStateFlow() + + fun updateUserProfile( + lastName: String, + firstName: String, + birthday: LocalDate, + job: String, + mbti: String, + ) { + _uiState.update { + it.copy( + userLastName = lastName, + userFirstName = firstName, + userBirthday = birthday, + userJob = job, + userMbti = mbti, + ) + } + } + + fun updateAiProfile( + age: String, + lastName: String, + firstName: String, + job: String, + mbti: String, + ) { + _uiState.update { + it.copy( + aiFirstName = firstName, + aiLastName = lastName, + aiAge = age, + aiJob = job, + aiMbti = mbti, + ) + } + } + + + fun updateConversationStyle( + speechStyle: SpeechStyle, + relationship: Relationship, + temperature: Int, + ) { + _uiState.update { + it.copy( + speechStyle = speechStyle, + relationship = relationship, + temperature = temperature, + ) + } + } + + + fun updateTraits(traits: List) { + _uiState.update { + it.copy(traits = traits) + } + } + + fun updatePreferTime(preferTime: PreferTime) { + _uiState.update { + it.copy(preferTime = preferTime) + } + } +} \ No newline at end of file From 4b869f6216ccb7702078d8ce291ad1f266586afe Mon Sep 17 00:00:00 2001 From: dada4 Date: Tue, 28 Jul 2026 13:44:51 +0900 Subject: [PATCH 08/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 15 ++++- .../call/impl/entry/OnboardingEntryBuilder.kt | 3 +- .../co/call/impl/screen/Onboarding4Screen.kt | 6 +- .../impl/viewmodel/OnboardingViewModel.kt | 60 ++++++++++--------- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index cf5be678..a25714c7 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay +import kr.co.call.api.CallRecordNavKey import kr.co.call.api.ChatRoomNavKey import kr.co.call.api.ChattingNavKey import kr.co.call.api.FaqNavKey @@ -146,13 +147,23 @@ fun AppScreen(modifier: Modifier = Modifier) { appNavigator.navigate(Onboarding6NavKey) }, onOnboarding6CallNow = { - // TODO: 실제 통화 화면으로 이동 + //나중에 전화화면으로 바꾸기 + appNavigator.navigate(HomeNavKey) }, onOnboarding6CallLater = { appNavigator.navigate(HomeNavKey) }, ) - homeEntry() + homeEntry( + navigateToCallRecord = { callId -> + appNavigator.navigate( + CallRecordNavKey(callId = callId), + ) + }, + onCallRecordBack = { + appNavigator.popBackStack() + }, + ) chattingEntry( navigateToChatRoom = { roomId -> appNavigator.navigate(ChatRoomNavKey(roomId = roomId)) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index c54e5224..67e1ae46 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -102,7 +102,8 @@ fun EntryProviderScope.onboardingEntry( } entry { - val uiState by onboardingViewModel.uiState.collectAsStateWithLifecycle() + val uiState by onboardingViewModel.container.stateFlow + .collectAsStateWithLifecycle() Onboarding6Screen( firstName = uiState.aiFirstName, onCallNowClick = onOnboarding6CallNow, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index e5164e7c..ab4c6356 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -110,7 +110,11 @@ fun Onboarding4Screen( bottom = 18.dp, ), text = "다음", - onClick = { onNextClick(selectedTraits) }, + enabled=selectedTraits.isNotEmpty(), + onClick = { if(selectedTraits.isNotEmpty()) { + onNextClick(selectedTraits) + } + }, ) } } diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt index 75853d45..0b30886c 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -2,21 +2,21 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.Trait import kr.co.call.impl.screen.Relationship import kr.co.call.impl.screen.SpeechStyle +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import javax.inject.Inject @HiltViewModel -class OnboardingViewModel @Inject constructor() : ViewModel() { - - private val _uiState = MutableStateFlow(OnboardingUiState()) - val uiState = _uiState.asStateFlow() +class OnboardingViewModel @Inject constructor() : ViewModel(), + ContainerHost { + override val container=container( + initialState= OnboardingUiState(), + ) fun updateUserProfile( lastName: String, @@ -24,17 +24,17 @@ class OnboardingViewModel @Inject constructor() : ViewModel() { birthday: LocalDate, job: String, mbti: String, - ) { - _uiState.update { - it.copy( - userLastName = lastName, - userFirstName = firstName, - userBirthday = birthday, - userJob = job, - userMbti = mbti, - ) + ) =intent{ + reduce{ + state.copy( + userLastName = lastName, + userFirstName = firstName, + userBirthday = birthday, + userJob = job, + userMbti = mbti, + ) + } } - } fun updateAiProfile( age: String, @@ -42,9 +42,9 @@ class OnboardingViewModel @Inject constructor() : ViewModel() { firstName: String, job: String, mbti: String, - ) { - _uiState.update { - it.copy( + )=intent { + reduce { + state.copy( aiFirstName = firstName, aiLastName = lastName, aiAge = age, @@ -59,9 +59,9 @@ class OnboardingViewModel @Inject constructor() : ViewModel() { speechStyle: SpeechStyle, relationship: Relationship, temperature: Int, - ) { - _uiState.update { - it.copy( + ) =intent { + reduce { + state.copy( speechStyle = speechStyle, relationship = relationship, temperature = temperature, @@ -70,15 +70,17 @@ class OnboardingViewModel @Inject constructor() : ViewModel() { } - fun updateTraits(traits: List) { - _uiState.update { - it.copy(traits = traits) + fun updateTraits(traits: List + )=intent{ + reduce { + state.copy(traits = traits) } } - fun updatePreferTime(preferTime: PreferTime) { - _uiState.update { - it.copy(preferTime = preferTime) + fun updatePreferTime(preferTime: PreferTime + )=intent { + reduce { + state.copy(preferTime = preferTime) } } } \ No newline at end of file From 10625e18d802dd5d61e68ca3f34114a10065ffe2 Mon Sep 17 00:00:00 2001 From: dada4 Date: Tue, 28 Jul 2026 16:08:03 +0900 Subject: [PATCH 09/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/kr/co/call/callfromai/AppScreen.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index a25714c7..df673f2b 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -3,7 +3,9 @@ package kr.co.call.callfromai import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing @@ -13,14 +15,17 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import kr.co.call.api.CallRecordNavKey import kr.co.call.api.ChatRoomNavKey @@ -102,7 +107,7 @@ fun AppScreen(modifier: Modifier = Modifier) { }, contentWindowInsets = WindowInsets.safeDrawing.only( WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal - ), + ).exclude(WindowInsets.ime), ) { padding -> @@ -112,6 +117,10 @@ fun AppScreen(modifier: Modifier = Modifier) { NavDisplay( backStack = backStack, modifier = Modifier.fillMaxSize().padding(padding), + entryDecorators=listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator (), + ), entryProvider = entryProvider { loginEntry() onboardingEntry( From 34666a89eca219e806efb9394faaf156d7c05dbf Mon Sep 17 00:00:00 2001 From: dada4 Date: Wed, 29 Jul 2026 17:36:41 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 4 +-- .../co/call/impl/component/BirthdayChoice.kt | 13 +++++----- .../co/call/impl/screen/Onboarding1Screen.kt | 25 +++++++++++-------- .../co/call/impl/screen/Onboarding4Screen.kt | 4 +-- .../call/impl/viewmodel/OnboardingUiState.kt | 3 +++ .../impl/viewmodel/OnboardingViewModel.kt | 13 ++++++++++ 6 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index cf76e9be..3b278c5f 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -3,6 +3,7 @@ package kr.co.call.callfromai import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.ime @@ -15,14 +16,12 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator @@ -51,7 +50,6 @@ import kr.co.call.impl.entry.homeEntry import kr.co.call.impl.entry.loginEntry import kr.co.call.impl.entry.myPageEntry import kr.co.call.impl.entry.onboardingEntry -import kr.co.call.callfromai.util.toMainTab import kr.co.call.impl.viewmodel.OnboardingViewModel /** diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt index 4fa035ef..5af29836 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -37,7 +37,7 @@ import androidx.compose.runtime.getValue @Composable fun BirthdayChoice( - selectedDate: LocalDate, + selectedDate: LocalDate?, onDateSelected: (LocalDate)->Unit, modifier: Modifier =Modifier, ){ @@ -80,8 +80,9 @@ fun BirthdayChoice( horizontalArrangement = Arrangement.SpaceBetween, ){ Text( - text=selectedDate.toDisplayText(), - color= Gray900, + text = selectedDate?.toDisplayText() + ?: "생년월일을 입력해주세요", + color = if (selectedDate == null) Gray600 else Gray900, style = CallTheme.typography.bodyMediumMedium, ) @@ -99,7 +100,7 @@ fun BirthdayChoice( if (showBottomWheel) { BottomWheel( - selectedDate = selectedDate, + selectedDate = selectedDate ?: LocalDate.now(), onConfirmClick = { newDate -> onDateSelected(newDate) showBottomWheel = false @@ -123,11 +124,11 @@ private fun LocalDate.toDisplayText(): String { private fun BirthdayChoicePreview() { CallFromAiTheme { BirthdayChoice( - selectedDate = LocalDate.of(2000, 1, 1), + selectedDate = null, onDateSelected = {}, modifier = Modifier .fillMaxWidth() .padding(24.dp), ) } -} \ No newline at end of file +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index 3a072154..c60e9107 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -63,7 +63,7 @@ fun Onboarding1Screen ( mutableStateOf("") } var birthday by remember { - mutableStateOf(LocalDate.of(2000, 1, 1)) + mutableStateOf(null) } var job by rememberSaveable { mutableStateOf("") @@ -79,7 +79,10 @@ fun Onboarding1Screen ( mutableStateOf("") } val canMoveNext = - lastName.isNotBlank() && firstName.isNotBlank() && job.isNotBlank() + lastName.isNotBlank() && + firstName.isNotBlank() && + birthday != null && + job.isNotBlank() Box( modifier = modifier.fillMaxSize() ) { @@ -189,13 +192,15 @@ fun Onboarding1Screen ( text = "다음", enabled = canMoveNext, onClick = { - onNextClick( - lastName, - firstName, - birthday, - job, - mbti, - ) + birthday?.let { selectedBirthday -> + onNextClick( + lastName, + firstName, + selectedBirthday, + job, + mbti, + ) + } }, ) } @@ -268,4 +273,4 @@ private fun Onboarding1ScreenPreview() { profileImageURl = null, ) } -} \ No newline at end of file +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index ab4c6356..4000a5fd 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -72,8 +72,8 @@ fun Onboarding4Screen( end = 27.dp, top = 20.dp, ), - horizontalArrangement = Arrangement.spacedBy(15.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), maxItemsInEachRow = 3, ) { Trait.entries.forEach { trait -> diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt index b1f07362..5c0db288 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -31,4 +31,7 @@ data class OnboardingUiState( //5단계 val preferTime: PreferTime? = null, + + val isCreatingAi: Boolean=false, + val createAiError: String?=null, ) \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt index 0b30886c..ca4ac9e4 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -77,10 +77,23 @@ class OnboardingViewModel @Inject constructor() : ViewModel(), } } + // fun updatePreferTime(preferTime: PreferTime )=intent { reduce { state.copy(preferTime = preferTime) } } + + // + fun onCreateAiSuccess(name: String + ) = intent { + reduce { + state.copy( + aiFirstName = name, + isCreatingAi = false, + createAiError = null, + ) + } + } } \ No newline at end of file From 2c6e21eebd98a38f2b5ff4ac78baa54aaad81346 Mon Sep 17 00:00:00 2001 From: dada4 Date: Thu, 30 Jul 2026 00:11:29 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt index 5c0db288..0c2603cc 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -1,5 +1,6 @@ package kr.co.call.impl.viewmodel +import androidx.compose.runtime.Composable import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.Trait import kr.co.call.impl.screen.Relationship From 94fb1582c8ced317817e01f240f9c2c77ad525bd Mon Sep 17 00:00:00 2001 From: dada4 Date: Thu, 30 Jul 2026 03:33:31 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20=EB=8B=A4=EC=8B=9C=20=EB=9E=9C?= =?UTF-8?q?=EB=94=A9=ED=99=94=EB=A9=B4=20=EB=AA=BB=EA=B0=80=EA=B2=8C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/kr/co/call/callfromai/AppScreen.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index ce9a35a6..3b4dcc06 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -131,16 +131,16 @@ fun AppScreen(modifier: Modifier = Modifier) { entryProvider = entryProvider { loginEntry( navigateToLogin={ - appNavigator.navigate(LoginNavKey) + appNavigator.replaceAll(LoginNavKey) }, navigateToOnboarding = { appNavigator.navigate(Onboarding1NavKey) }, navigateToHome = { - appNavigator.navigate(HomeNavKey) + appNavigator.replaceAll(HomeNavKey) }, navigateToAgreement={ - appNavigator.navigate(AgreementNavKey) + appNavigator.replaceAll(AgreementNavKey) }, navigateToAgreementDetail={term -> appNavigator.navigate( @@ -192,10 +192,10 @@ fun AppScreen(modifier: Modifier = Modifier) { }, onOnboarding6CallNow = { //나중에 전화화면으로 바꾸기 - appNavigator.navigate(HomeNavKey) + appNavigator.replaceAll(HomeNavKey) }, onOnboarding6CallLater = { - appNavigator.navigate(HomeNavKey) + appNavigator.replaceAll(HomeNavKey) }, ) homeEntry( From 37b54a779c2d28e43f08392a4ecf96488e002b27 Mon Sep 17 00:00:00 2001 From: dada4 Date: Thu, 30 Jul 2026 04:42:50 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/call/impl/component/BirthdayChoice.kt | 6 ++- .../kr/co/call/impl/component/BottomWheel.kt | 53 ++++++++++++++++--- .../call/impl/entry/OnboardingEntryBuilder.kt | 24 ++++----- .../co/call/impl/screen/Onboarding1Screen.kt | 49 +++++------------ .../co/call/impl/screen/Onboarding2Screen.kt | 46 ++++------------ .../co/call/impl/screen/Onboarding3Screen.kt | 13 +---- .../call/impl/viewmodel/OnboardingUiState.kt | 6 +-- .../impl/viewmodel/OnboardingViewModel.kt | 4 +- .../kr/co/call/impl/viewmodel/model/Mbti.kt | 20 +++++++ .../call/impl/viewmodel/model/Relationship.kt | 7 +++ .../call/impl/viewmodel/model/SpeechStyle.kt | 7 +++ .../impl/viewmodel/state/Onboarding1State.kt | 11 ++++ .../impl/viewmodel/state/Onboarding2State.kt | 9 ++++ 13 files changed, 146 insertions(+), 109 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Mbti.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Relationship.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/SpeechStyle.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt index 5af29836..e0572335 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -100,9 +100,11 @@ fun BirthdayChoice( if (showBottomWheel) { BottomWheel( - selectedDate = selectedDate ?: LocalDate.now(), + selectedDate = selectedDate, onConfirmClick = { newDate -> - onDateSelected(newDate) + if(newDate != null) { + onDateSelected(newDate) + } showBottomWheel = false }, onDismissRequest = { diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt index dc00ba4d..6df56cfc 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -18,16 +19,19 @@ import kr.co.call.designsystem.theme.CallTheme import java.time.LocalDate import java.time.YearMonth import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.tooling.preview.Preview +import kotlinx.coroutines.flow.filter import kr.co.call.designsystem.component.bottomsheet.ConfirmBottomSheet import kr.co.call.designsystem.component.picker.WheelPicker import kr.co.call.designsystem.theme.CallFromAiTheme @Composable fun BottomWheel( - selectedDate: LocalDate, - onConfirmClick: (LocalDate) -> Unit, + selectedDate: LocalDate?, + onConfirmClick: (LocalDate?) -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ){ @@ -35,28 +39,63 @@ fun BottomWheel( (1900..LocalDate.now().year).toList() } + val initialDate = selectedDate ?: LocalDate.now() + var draftDate by remember(selectedDate) { - mutableStateOf(selectedDate) + mutableStateOf(initialDate) } val yearState = rememberLazyListState( initialFirstVisibleItemIndex = years - .indexOf(selectedDate.year) + .indexOf(initialDate.year) .coerceAtLeast(0), ) val monthState = rememberLazyListState( - initialFirstVisibleItemIndex = selectedDate.monthValue - 1, + initialFirstVisibleItemIndex = initialDate.monthValue - 1, ) val dayState = rememberLazyListState( - initialFirstVisibleItemIndex = selectedDate.dayOfMonth - 1, + initialFirstVisibleItemIndex = initialDate.dayOfMonth - 1, ) + //사용자가 휠 움직였는지 기록 + var hasUserChangedDate by rememberSaveable { + mutableStateOf(false) + } + LaunchedEffect(yearState) { + snapshotFlow { yearState.isScrollInProgress } + .filter { it } + .collect { + hasUserChangedDate = true + } + } + + LaunchedEffect(monthState) { + snapshotFlow { monthState.isScrollInProgress } + .filter { it } + .collect { + hasUserChangedDate = true + } + } + + LaunchedEffect(dayState) { + snapshotFlow { dayState.isScrollInProgress } + .filter { it } + .collect { + hasUserChangedDate = true + } + } + ConfirmBottomSheet( title = "생년월일 선택", modifier = modifier, onDismissRequest = onDismissRequest, onConfirmClick = { - onConfirmClick(draftDate) + val confirmedDate = when { + selectedDate != null -> draftDate + hasUserChangedDate -> draftDate + else -> null + } + onConfirmClick(confirmedDate) }, ) { Box( diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index 67e1ae46..71bca3e6 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -36,13 +36,13 @@ fun EntryProviderScope.onboardingEntry( entry { Onboarding1Screen( onProfileClick = onProfileClick, - onNextClick = { lastName, firstName, birthday, job, mbti -> + onNextClick = {state -> onboardingViewModel.updateUserProfile( - lastName = lastName, - firstName = firstName, - birthday = birthday, - job = job, - mbti = mbti, + lastName = state.lastName, + firstName = state.firstName, + birthday = state.birthday, + job = state.job, + mbti = state.mbti, ) onOnboarding1Next() }, @@ -53,13 +53,13 @@ fun EntryProviderScope.onboardingEntry( Onboarding2Screen( onBackClick = onBackFromOnboarding2, onProfileClick = onProfileClick, - onNextClick ={ age, lastName, firstName, job, mbti -> + onNextClick ={ state -> onboardingViewModel.updateAiProfile( - age = age, - lastName = lastName, - firstName = firstName, - job = job, - mbti = mbti, + age = state.age, + lastName = state.lastName, + firstName = state.firstName, + job = state.job, + mbti = state.mbti, ) onOnboarding2Next() diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index c60e9107..83e593a3 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -38,6 +38,8 @@ import kr.co.call.impl.component.MessageInputField import kr.co.call.impl.component.NameBox import kr.co.call.impl.component.ProfileChoice import kr.co.call.impl.component.TopTitle +import kr.co.call.impl.viewmodel.model.Mbti +import kr.co.call.impl.viewmodel.state.Onboarding1State private enum class EditingNameField { LAST_NAME, @@ -46,13 +48,7 @@ private enum class EditingNameField { @Composable fun Onboarding1Screen ( onProfileClick:()->Unit, - onNextClick: ( - lastName: String, - firstName: String, - birthday: LocalDate, - job: String, - mbti: String, - )->Unit, + onNextClick: (Onboarding1State)->Unit, modifier: Modifier =Modifier, profileImageURl: String?=null, ) { @@ -166,12 +162,7 @@ fun Onboarding1Screen ( label = "MBTI", selectedOption = mbti, placeholder = "MBTI를 선택해주세요", - options = listOf( - "INFP", "INFJ", "ENFP", "ENFJ", - "INTP", "INTJ", "ENTP", "ENTJ", - "ISFP", "ISFJ", "ESFP", "ESFJ", - "ISTP", "ISTJ", "ESTP", "ESTJ", - ), + options = Mbti.entries.map { it.name }, onOptionSelected = { mbti = it }, ) } @@ -194,11 +185,13 @@ fun Onboarding1Screen ( onClick = { birthday?.let { selectedBirthday -> onNextClick( - lastName, - firstName, - selectedBirthday, - job, - mbti, + Onboarding1State( + lastName=lastName, + firstName=firstName, + birthday=selectedBirthday, + job=job, + mbti=mbti, + ) ) } }, @@ -255,22 +248,4 @@ fun Onboarding1Screen ( ) } } -} - -@Preview( - name = "Onboarding 1 Screen", - showBackground = true, - backgroundColor = 0xFFFFFFFF, -) -@Composable -private fun Onboarding1ScreenPreview() { - CallFromAiTheme { - Onboarding1Screen( - onProfileClick = {}, - onNextClick = { lastName, firstName, birthday, job, mbti -> - // Preview에서는 동작하지 않음 - }, - profileImageURl = null, - ) - } -} +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index 4f58afab..41046a3f 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -40,6 +40,8 @@ import kr.co.call.impl.component.MessageInputField import kr.co.call.impl.component.NameBox import kr.co.call.impl.component.ProfileChoice import kr.co.call.impl.component.TopTitle +import kr.co.call.impl.viewmodel.model.Mbti +import kr.co.call.impl.viewmodel.state.Onboarding2State private enum class Onboarding2EditingNameField { @@ -50,13 +52,7 @@ private enum class Onboarding2EditingNameField { fun Onboarding2Screen( onBackClick: () -> Unit, onProfileClick: () -> Unit, - onNextClick: ( - age: String, - lastName: String, - firstName: String, - job: String, - mbti: String, - ) -> Unit, + onNextClick: (Onboarding2State) -> Unit, modifier: Modifier = Modifier, profileImageUrl: String? = null, ) { @@ -169,12 +165,7 @@ fun Onboarding2Screen( label = "MBTI", selectedOption = mbti, placeholder = "MBTI를 선택해주세요", - options = listOf( - "INFP", "INFJ", "ENFP", "ENFJ", - "INTP", "INTJ", "ENTP", "ENTJ", - "ISFP", "ISFJ", "ESFP", "ESFJ", - "ISTP", "ISTJ", "ESTP", "ESTJ", - ), + options = Mbti.entries.map { it.name }, onOptionSelected = { mbti = it }, ) } @@ -201,11 +192,13 @@ fun Onboarding2Screen( enabled = canMoveNext, onClick = { onNextClick( - age, - lastName, - firstName, - job, - mbti, + Onboarding2State( + age=age, + lastName=lastName, + firstName=firstName, + job=job, + mbti=mbti, + ) ) }, ) @@ -254,20 +247,3 @@ fun Onboarding2Screen( } } } - -@Preview( - name = "Onboarding 2 Screen", - showBackground = true, - widthDp = 393, - heightDp = 852, -) -@Composable -private fun Onboarding2ScreenPreview() { - CallFromAiTheme { - Onboarding2Screen( - onBackClick = {}, - onProfileClick = {}, - onNextClick = { _, _, _, _, _ -> }, - ) - } -} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt index ed88564a..6dc07431 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt @@ -33,19 +33,10 @@ import kr.co.call.impl.component.RelationshipChoiceCard import kr.co.call.impl.component.SpeechChoice import kr.co.call.impl.component.TempControl import kr.co.call.impl.component.TopTitle +import kr.co.call.impl.viewmodel.model.Relationship +import kr.co.call.impl.viewmodel.model.SpeechStyle import kr.co.call.onboarding.impl.R -enum class SpeechStyle{ - CASUAL, - SEMI_FORMAL, - FORMAL -} - -enum class Relationship{ - SOME, - EARLY_DATING, - LONG_TERM -} @Composable fun Onboarding3Screen( diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt index 0c2603cc..2ccb35f9 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -3,8 +3,8 @@ package kr.co.call.impl.viewmodel import androidx.compose.runtime.Composable import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.Trait -import kr.co.call.impl.screen.Relationship -import kr.co.call.impl.screen.SpeechStyle +import kr.co.call.impl.viewmodel.model.Relationship +import kr.co.call.impl.viewmodel.model.SpeechStyle import java.time.LocalDate data class OnboardingUiState( @@ -32,7 +32,7 @@ data class OnboardingUiState( //5단계 val preferTime: PreferTime? = null, - + val isCreatingAi: Boolean=false, val createAiError: String?=null, ) \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt index ca4ac9e4..7835eeaa 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kr.co.call.impl.component.PreferTime import kr.co.call.impl.component.Trait -import kr.co.call.impl.screen.Relationship -import kr.co.call.impl.screen.SpeechStyle +import kr.co.call.impl.viewmodel.model.Relationship +import kr.co.call.impl.viewmodel.model.SpeechStyle import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Mbti.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Mbti.kt new file mode 100644 index 00000000..8707e598 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Mbti.kt @@ -0,0 +1,20 @@ +package kr.co.call.impl.viewmodel.model + +enum class Mbti { + INFP, + INFJ, + ENFP, + ENFJ, + INTP, + INTJ, + ENTP, + ENTJ, + ISFP, + ISFJ, + ESFP, + ESFJ, + ISTP, + ISTJ, + ESTP, + ESTJ, +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Relationship.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Relationship.kt new file mode 100644 index 00000000..f87614eb --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Relationship.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.viewmodel.model + +enum class Relationship{ + SOME, + EARLY_DATING, + LONG_TERM +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/SpeechStyle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/SpeechStyle.kt new file mode 100644 index 00000000..1514f8ed --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/SpeechStyle.kt @@ -0,0 +1,7 @@ +package kr.co.call.impl.viewmodel.model + +enum class SpeechStyle{ + CASUAL, + SEMI_FORMAL, + FORMAL +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt new file mode 100644 index 00000000..18f0c7be --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding1State.kt @@ -0,0 +1,11 @@ +package kr.co.call.impl.viewmodel.state + +import java.time.LocalDate + +data class Onboarding1State ( + val lastName: String="", + val firstName: String="", + val birthday: LocalDate=LocalDate.now(), + val job: String="", + val mbti: String="", +) \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt new file mode 100644 index 00000000..e56608d8 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/Onboarding2State.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel.state + +data class Onboarding2State ( + val age: String="", + val lastName: String="", + val firstName: String="", + val job: String="", + val mbti: String="", + ) \ No newline at end of file From 19cbbe0a966a3d63cc038545d91c75c790cf2f0f Mon Sep 17 00:00:00 2001 From: dada4 Date: Fri, 31 Jul 2026 01:39:03 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 5 ---- .../co/call/impl/component/ProfileChoice.kt | 27 +++++-------------- .../call/impl/entry/OnboardingEntryBuilder.kt | 22 +++++++++++++-- .../co/call/impl/screen/Onboarding1Screen.kt | 6 +++-- .../co/call/impl/screen/Onboarding2Screen.kt | 6 +++-- .../viewmodel/state/ProfileChoiceState.kt | 9 +++++++ 6 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/ProfileChoiceState.kt diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 3b4dcc06..f378c0e2 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator @@ -59,7 +58,6 @@ import kr.co.call.impl.entry.homeEntry import kr.co.call.impl.entry.loginEntry import kr.co.call.impl.entry.myPageEntry import kr.co.call.impl.entry.onboardingEntry -import kr.co.call.impl.viewmodel.OnboardingViewModel /** * 애플리케이션 화면 내비게이션의 메인 진입점입니다. @@ -100,8 +98,6 @@ fun AppScreen(modifier: Modifier = Modifier) { val bottomBarPadding = remember(bottomBarHeightPx) { with(density) { bottomBarHeightPx.toDp() } } - - val onboardingViewModel: OnboardingViewModel = hiltViewModel() Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { @@ -159,7 +155,6 @@ fun AppScreen(modifier: Modifier = Modifier) { } ) onboardingEntry( - onboardingViewModel=onboardingViewModel, onProfileClick = { // 사진 선택 기능은 나중에 구현 }, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt index ee53cdcc..eac827e7 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/ProfileChoice.kt @@ -23,27 +23,27 @@ import coil.compose.AsyncImage import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.onboarding.impl.R import androidx.compose.runtime.getValue +import kr.co.call.impl.viewmodel.state.ProfileChoiceState @Composable fun ProfileChoice( - imageUrl: String?, + state: ProfileChoiceState, onClick: ()->Unit, - size: Dp =99.dp, - modifier: Modifier, + modifier: Modifier=Modifier, ){ val interactionSource=remember{MutableInteractionSource()} val isPressed by interactionSource.collectIsPressedAsState() Box( modifier=modifier - .size(size) + .size(state.size) .clickable( interactionSource=interactionSource, indication = null, onClick=onClick, ) ) { - if (imageUrl.isNullOrEmpty()){ + if (state.imageUrl.isNullOrEmpty()){ Image( imageVector= ImageVector.vectorResource( id= if(isPressed){ @@ -56,7 +56,7 @@ fun ProfileChoice( ) } else{ AsyncImage( - model=imageUrl, + model=state.imageUrl, contentDescription="선택된 프로필 사진", modifier=Modifier .fillMaxSize() @@ -66,18 +66,3 @@ fun ProfileChoice( } } } - -@Preview( - name = "기본 프로필 이미지", - showBackground = true, -) -@Composable -private fun ProfileChoicePreview() { - CallFromAiTheme { - ProfileChoice( - modifier = Modifier.padding(20.dp), - imageUrl = null, - onClick = {}, - ) - } -} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index 71bca3e6..dba7b2ac 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -1,5 +1,8 @@ package kr.co.call.impl.entry +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey @@ -15,11 +18,11 @@ import kr.co.call.impl.screen.Onboarding3Screen import kr.co.call.impl.screen.Onboarding4Screen import kr.co.call.impl.screen.Onboarding5Screen import kr.co.call.impl.screen.Onboarding6Screen -import kr.co.call.impl.viewmodel.OnboardingViewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.compose.runtime.getValue +import kr.co.call.impl.viewmodel.OnboardingViewModel fun EntryProviderScope.onboardingEntry( - onboardingViewModel: OnboardingViewModel, onProfileClick: () -> Unit, onOnboarding1Next: () -> Unit, onBackFromOnboarding2: () -> Unit, @@ -34,6 +37,7 @@ fun EntryProviderScope.onboardingEntry( onOnboarding6CallLater: () -> Unit, ) { entry { + val onboardingViewModel = sharedOnboardingViewModel() Onboarding1Screen( onProfileClick = onProfileClick, onNextClick = {state -> @@ -50,6 +54,7 @@ fun EntryProviderScope.onboardingEntry( } entry { + val onboardingViewModel = sharedOnboardingViewModel() Onboarding2Screen( onBackClick = onBackFromOnboarding2, onProfileClick = onProfileClick, @@ -68,6 +73,7 @@ fun EntryProviderScope.onboardingEntry( } entry { + val onboardingViewModel = sharedOnboardingViewModel() Onboarding3Screen( onBackClick = onBackFromOnboarding3, onNextClick = { speechStyle, relationship, temperature -> @@ -82,6 +88,7 @@ fun EntryProviderScope.onboardingEntry( } entry { + val onboardingViewModel = sharedOnboardingViewModel() Onboarding4Screen( onBackClick = onBackFromOnboarding4, onNextClick = { traits -> @@ -92,6 +99,7 @@ fun EntryProviderScope.onboardingEntry( } entry { + val onboardingViewModel = sharedOnboardingViewModel() Onboarding5Screen( onBackClick = onBackFromOnboarding5, onNextClick = { preferTime -> @@ -102,6 +110,7 @@ fun EntryProviderScope.onboardingEntry( } entry { + val onboardingViewModel = sharedOnboardingViewModel() val uiState by onboardingViewModel.container.stateFlow .collectAsStateWithLifecycle() Onboarding6Screen( @@ -111,3 +120,12 @@ fun EntryProviderScope.onboardingEntry( ) } } + +@Composable +private fun sharedOnboardingViewModel(): OnboardingViewModel { + val activity = checkNotNull(LocalActivity.current as? ComponentActivity) + + return hiltViewModel( + viewModelStoreOwner =activity, + ) +} diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index 83e593a3..c738b57c 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -40,6 +40,7 @@ import kr.co.call.impl.component.ProfileChoice import kr.co.call.impl.component.TopTitle import kr.co.call.impl.viewmodel.model.Mbti import kr.co.call.impl.viewmodel.state.Onboarding1State +import kr.co.call.impl.viewmodel.state.ProfileChoiceState private enum class EditingNameField { LAST_NAME, @@ -110,10 +111,11 @@ fun Onboarding1Screen ( ) Spacer(modifier = Modifier.height(18.dp)) ProfileChoice( - modifier = Modifier, + state= ProfileChoiceState( imageUrl = profileImageURl, + size=99.dp, + ), onClick = onProfileClick, - size = 99.dp, ) Spacer(modifier = Modifier.height(24.dp)) Row( diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index 41046a3f..b460cdcc 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -42,6 +42,7 @@ import kr.co.call.impl.component.ProfileChoice import kr.co.call.impl.component.TopTitle import kr.co.call.impl.viewmodel.model.Mbti import kr.co.call.impl.viewmodel.state.Onboarding2State +import kr.co.call.impl.viewmodel.state.ProfileChoiceState private enum class Onboarding2EditingNameField { @@ -110,8 +111,9 @@ fun Onboarding2Screen( Spacer(modifier = Modifier.height(18.dp)) ProfileChoice( - modifier = Modifier, + state= ProfileChoiceState( imageUrl = profileImageUrl, + ), onClick = onProfileClick, ) @@ -153,7 +155,7 @@ fun Onboarding2Screen( label = "직업", selectedOption = job, placeholder = "직업을 선택해주세요", - options = listOf("학생", "직장인", "기타"), + options = listOf("대학생", "직장인", "기타"), onOptionSelected = { job = it }, required = true, ) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/ProfileChoiceState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/ProfileChoiceState.kt new file mode 100644 index 00000000..a87d574f --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/state/ProfileChoiceState.kt @@ -0,0 +1,9 @@ +package kr.co.call.impl.viewmodel.state + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class ProfileChoiceState( + val imageUrl: String? = null, + val size: Dp = 99.dp, +) \ No newline at end of file From e7fe9bc232fc5780a3ddea4147fe98baba40bc5f Mon Sep 17 00:00:00 2001 From: dada4 Date: Fri, 31 Jul 2026 02:18:55 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20pr=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 2 +- .../kr/co/call/impl/component/BottomWheel.kt | 18 ---------- .../co/call/impl/component/KeywordChoice.kt | 36 +------------------ .../co/call/impl/screen/Onboarding4Screen.kt | 7 ++-- .../co/call/impl/screen/Onboarding5Screen.kt | 4 --- .../call/impl/viewmodel/OnboardingUiState.kt | 2 +- .../impl/viewmodel/OnboardingViewModel.kt | 2 +- .../kr/co/call/impl/viewmodel/model/Trait.kt | 34 ++++++++++++++++++ 8 files changed, 41 insertions(+), 64 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Trait.kt diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index f378c0e2..57593db4 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -130,7 +130,7 @@ fun AppScreen(modifier: Modifier = Modifier) { appNavigator.replaceAll(LoginNavKey) }, navigateToOnboarding = { - appNavigator.navigate(Onboarding1NavKey) + appNavigator.replaceAll(Onboarding1NavKey) }, navigateToHome = { appNavigator.replaceAll(HomeNavKey) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt index 6df56cfc..eb8b8b03 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt @@ -182,21 +182,3 @@ fun BottomWheel( } } } - - -@Preview( - name = "생년월일 선택", - showBackground = true, - widthDp = 412, - heightDp = 800, -) -@Composable -private fun BottomWheelPreview() { - CallFromAiTheme { - BottomWheel( - selectedDate = LocalDate.of(2001, 5, 18), - onConfirmClick = {}, - onDismissRequest = {}, - ) - } -} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt index ce08fbab..751f2a76 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt @@ -25,42 +25,8 @@ import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.theme.Gray200 import kr.co.call.designsystem.theme.MainVariant1 -import kr.co.call.designsystem.theme.SubGray import kr.co.call.designsystem.theme.White - -enum class Trait( - val keyword: String, - val emoji: String, - val label: String, -){ - HUMOROUS("HUMOROUS", "😃", "유머러스한"), - PLAYFUL("PLAYFUL", "🤡", "장난기 많은"), - AFFECTIONATE("AFFECTIONATE", "💕", "애교 많은"), - JEALOUS("JEALOUS", "❤️‍🔥", "질투심 폭발"), - TALKATIVE("TALKATIVE", "🗣️", "수다쟁이"), - DAD_JOKE_LOVER("DAD_JOKE_LOVER", "😝", "아재개그 좋아하는"), - HOMEBODY("HOMEBODY", "🛌", "집순이/집돌이"), - TEASING("TEASING", "🏀", "놀리는 걸 좋아하는"), - POSSESSIVE("POSSESSIVE", "📱", "집착하는"), - TSUNDERE("TSUNDERE", "👋", "츤데레"), - EXPRESSIVE("EXPRESSIVE", "😌", "표현을 많이 하는"), - PET_NAME_LOVER("PET_NAME_LOVER", "👄", "애칭을 자주 쓰는"), - EXCLUSIVE("EXCLUSIVE", "🪴", "독점욕이 있는"), - QUIRKY("QUIRKY", "👍", "4차원 같은"), - LAID_BACK("LAID_BACK", "🕶️", "털털한"), - OPENLY_JEALOUS("OPENLY_JEALOUS", "📑", "질투를 숨기지 않는"), - SHY("SHY", "🧐", "부끄러움을 많이 타는"), - SMOOTH_TALKER("SMOOTH_TALKER", "☄\uFE0F", "능청스러운"), - FREQUENT_CHECKER("FREQUENT_CHECKER", "💌", "연락을 자주 확인하는"), - GOOD_LISTENER("GOOD_LISTENER", "👐", "고민을 잘 들어주는"), - COMPLIMENTER("COMPLIMENTER", "😍", "칭찬을 많이 하는"); - - companion object { - fun fromKeyword(key: String): Trait? { - return entries.find { it.keyword == key } - } - } -} +import kr.co.call.impl.viewmodel.model.Trait @Composable fun KeywordChoice( diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index 4000a5fd..85b3e256 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -15,12 +15,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import kr.co.call.impl.component.Trait import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kr.co.call.designsystem.component.button.PrimaryButton import kr.co.call.designsystem.component.button.SecondaryButton import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.SubGray2 @@ -28,6 +26,7 @@ import kr.co.call.designsystem.theme.White import kr.co.call.impl.component.BackStepBar import kr.co.call.impl.component.KeywordChoice import kr.co.call.impl.component.TopTitle +import kr.co.call.impl.viewmodel.model.Trait @OptIn(ExperimentalLayoutApi::class) @Composable @@ -72,8 +71,8 @@ fun Onboarding4Screen( end = 27.dp, top = 20.dp, ), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(22.dp), maxItemsInEachRow = 3, ) { Trait.entries.forEach { trait -> diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt index c682ccf2..0e2bb017 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -38,10 +38,6 @@ fun Onboarding5Screen( Column( modifier = modifier .fillMaxSize() - .padding( - top = 54.dp, - bottom = 18.dp, - ) .background(White), ) { BackStepBar(onBackClick = onBackClick) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt index 2ccb35f9..b24cfd34 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -2,7 +2,7 @@ package kr.co.call.impl.viewmodel import androidx.compose.runtime.Composable import kr.co.call.impl.component.PreferTime -import kr.co.call.impl.component.Trait +import kr.co.call.impl.viewmodel.model.Trait import kr.co.call.impl.viewmodel.model.Relationship import kr.co.call.impl.viewmodel.model.SpeechStyle import java.time.LocalDate diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt index 7835eeaa..8b1628c7 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -3,7 +3,7 @@ package kr.co.call.impl.viewmodel import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kr.co.call.impl.component.PreferTime -import kr.co.call.impl.component.Trait +import kr.co.call.impl.viewmodel.model.Trait import kr.co.call.impl.viewmodel.model.Relationship import kr.co.call.impl.viewmodel.model.SpeechStyle import org.orbitmvi.orbit.ContainerHost diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Trait.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Trait.kt new file mode 100644 index 00000000..a9b5f3cd --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/model/Trait.kt @@ -0,0 +1,34 @@ +package kr.co.call.impl.viewmodel.model + +enum class Trait( + val keyword: String, + val emoji: String, + val label: String, +){ + HUMOROUS("HUMOROUS", "😃", "유머러스한"), + PLAYFUL("PLAYFUL", "\uD83E\uDD21", "장난기 많은"), + AFFECTIONATE("AFFECTIONATE", "\uD83D\uDC95", "애교 많은"), + JEALOUS("JEALOUS", "❤\uFE0F\u200D\uD83D\uDD25", "질투심 폭발"), + TALKATIVE("TALKATIVE", "\uD83E\uDDCF\uD83C\uDFFB", "수다쟁이"), + DAD_JOKE_LOVER("DAD_JOKE_LOVER", "\uD83D\uDE1D", "아재개그 좋아하는"), + HOMEBODY("HOMEBODY", "\uD83D\uDECF\uFE0F", "집순이/집돌이"), + TEASING("TEASING", "\uD83C\uDFC0", "놀리는 걸 좋아하는"), + POSSESSIVE("POSSESSIVE", "\uD83D\uDCF1", "집착하는"), + TSUNDERE("TSUNDERE", "\uD83D\uDC4B\uD83C\uDFFB", "츤데레"), + EXPRESSIVE("EXPRESSIVE", "\uD83D\uDE0C", "표현을 많이 하는"), + PET_NAME_LOVER("PET_NAME_LOVER", "\uD83E\uDEE6", "애칭을 자주 쓰는"), + EXCLUSIVE("EXCLUSIVE", "\uD83E\uDEE6", "독점욕이 있는"), + QUIRKY("QUIRKY", "\uD83D\uDC4D\uD83C\uDFFB", "4차원 같은"), + LAID_BACK("LAID_BACK", "\uD83D\uDD76\uFE0F", "털털한"), + OPENLY_JEALOUS("OPENLY_JEALOUS", "\uD83D\uDCD1", "질투를 숨기지 않는"), + SHY("SHY", "\uD83E\uDDD0", "부끄러움을 많이 타는"), + SMOOTH_TALKER("SMOOTH_TALKER", "☄\uFE0F", "능청스러운"), + FREQUENT_CHECKER("FREQUENT_CHECKER", "\uD83D\uDC8C", "연락을 자주 확인하는"), + GOOD_LISTENER("GOOD_LISTENER", "\uD83D\uDC50\uD83C\uDFFB", "고민을 잘 들어주는"), + COMPLIMENTER("COMPLIMENTER", "\uD83D\uDC50\uD83C\uDFFB", "칭찬을 많이 하는"); + + companion object { + fun fromKeyword(key: String): Trait? { + return entries.find { it.keyword == key } } + } + } \ No newline at end of file From dc00ec2593f8f3dcf81a09c175be9aaa8df8ab0a Mon Sep 17 00:00:00 2001 From: dada4 Date: Fri, 31 Jul 2026 03:26:33 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20BottomModal=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/co/call/callfromai/AppScreen.kt | 3 - .../component/TemporaryProfileImageData.kt | 29 ++++++++ .../call/impl/entry/OnboardingEntryBuilder.kt | 3 - .../co/call/impl/screen/Onboarding1Screen.kt | 67 +++++++++++++++-- .../co/call/impl/screen/Onboarding2Screen.kt | 71 ++++++++++++++++++- 5 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt diff --git a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt index 5bf8fd02..0c38192b 100644 --- a/app/src/main/java/kr/co/call/callfromai/AppScreen.kt +++ b/app/src/main/java/kr/co/call/callfromai/AppScreen.kt @@ -157,9 +157,6 @@ fun AppScreen(modifier: Modifier = Modifier) { } ) onboardingEntry( - onProfileClick = { - // 사진 선택 기능은 나중에 구현 - }, onOnboarding1Next= { appNavigator.navigate(Onboarding2NavKey) }, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt new file mode 100644 index 00000000..16618ddd --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TemporaryProfileImageData.kt @@ -0,0 +1,29 @@ +package kr.co.call.impl.component + +import kr.co.call.designsystem.component.profileimage.ProfileImageGender +import kr.co.call.designsystem.component.profileimage.ProfileImageOption + +//서버에서 사진 받아오기 전까지 임시로 사용 +internal object TemporaryProfileImageData { + + val maleImages = List(5) { index -> + ProfileImageOption( + id = "temporary_male_${index + 1}", + imageUrl = "", + ) + } + + val femaleImages = List(5) { index -> + ProfileImageOption( + id = "temporary_female_${index + 1}", + imageUrl = "", + ) + } + + fun imagesFor( + gender: ProfileImageGender, + ): List = when (gender) { + ProfileImageGender.MALE -> maleImages + ProfileImageGender.FEMALE -> femaleImages + } +} \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index dba7b2ac..97887cc9 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.getValue import kr.co.call.impl.viewmodel.OnboardingViewModel fun EntryProviderScope.onboardingEntry( - onProfileClick: () -> Unit, onOnboarding1Next: () -> Unit, onBackFromOnboarding2: () -> Unit, onOnboarding2Next: () -> Unit, @@ -39,7 +38,6 @@ fun EntryProviderScope.onboardingEntry( entry { val onboardingViewModel = sharedOnboardingViewModel() Onboarding1Screen( - onProfileClick = onProfileClick, onNextClick = {state -> onboardingViewModel.updateUserProfile( lastName = state.lastName, @@ -57,7 +55,6 @@ fun EntryProviderScope.onboardingEntry( val onboardingViewModel = sharedOnboardingViewModel() Onboarding2Screen( onBackClick = onBackFromOnboarding2, - onProfileClick = onProfileClick, onNextClick ={ state -> onboardingViewModel.updateAiProfile( age = state.age, diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt index c738b57c..36214009 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -27,7 +27,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.selects.select +import kr.co.call.designsystem.component.bottomsheet.ProfileImagePickerBottomSheet import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.component.profileimage.ProfileImageGender import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.theme.Gray400 @@ -37,6 +40,7 @@ import kr.co.call.impl.component.MemberChoice import kr.co.call.impl.component.MessageInputField import kr.co.call.impl.component.NameBox import kr.co.call.impl.component.ProfileChoice +import kr.co.call.impl.component.TemporaryProfileImageData import kr.co.call.impl.component.TopTitle import kr.co.call.impl.viewmodel.model.Mbti import kr.co.call.impl.viewmodel.state.Onboarding1State @@ -48,10 +52,9 @@ private enum class EditingNameField { } @Composable fun Onboarding1Screen ( - onProfileClick:()->Unit, onNextClick: (Onboarding1State)->Unit, modifier: Modifier =Modifier, - profileImageURl: String?=null, + profileImageUrl: String?=null, ) { var lastName by rememberSaveable { mutableStateOf("") @@ -75,6 +78,25 @@ fun Onboarding1Screen ( var nameInput by rememberSaveable { mutableStateOf("") } + var showProfileImagePicker by rememberSaveable { + mutableStateOf(false) + } + + var selectedGender by rememberSaveable { + mutableStateOf(ProfileImageGender.MALE) + } + + var selectedImageId by rememberSaveable { + mutableStateOf(null) + } + + var savedProfileImageUrl by rememberSaveable { + mutableStateOf(profileImageUrl) + } + + val currentProfileImages = + TemporaryProfileImageData.imagesFor(selectedGender) + val canMoveNext = lastName.isNotBlank() && firstName.isNotBlank() && @@ -112,10 +134,15 @@ fun Onboarding1Screen ( Spacer(modifier = Modifier.height(18.dp)) ProfileChoice( state= ProfileChoiceState( - imageUrl = profileImageURl, + imageUrl = savedProfileImageUrl, size=99.dp, ), - onClick = onProfileClick, + onClick ={ + if (selectedImageId==null){ + selectedImageId=currentProfileImages.firstOrNull()?.id + } + showProfileImagePicker=true + }, ) Spacer(modifier = Modifier.height(24.dp)) Row( @@ -249,5 +276,37 @@ fun Onboarding1Screen ( ), ) } + if (showProfileImagePicker) { + ProfileImagePickerBottomSheet( + images = currentProfileImages, + selectedGender = selectedGender, + selectedImageId = selectedImageId, + onGenderChange = { newGender -> + selectedGender = newGender + + selectedImageId = TemporaryProfileImageData + .imagesFor(newGender) + .firstOrNull() + ?.id + }, + onImageSelected = { image -> + selectedImageId = image.id + }, + onSaveClick = { + savedProfileImageUrl = currentProfileImages + .firstOrNull { image -> + image.id == selectedImageId + } + ?.imageUrl + + showProfileImagePicker = false + }, + onDismissRequest = { + showProfileImagePicker = false + }, + title = "원하는 사진을\n선택해주세요", + confirmText = "저장하기", + ) + } } } \ No newline at end of file diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt index b460cdcc..612fb4bd 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -27,7 +27,10 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kr.co.call.designsystem.component.bottomsheet.ProfileImagePickerBottomSheet import kr.co.call.designsystem.component.button.SecondaryButton +import kr.co.call.designsystem.component.profileimage.ProfileImageGender +import kr.co.call.designsystem.component.profileimage.ProfileImageOption import kr.co.call.designsystem.theme.CallFromAiTheme import kr.co.call.designsystem.theme.CallTheme import kr.co.call.designsystem.theme.Gray400 @@ -39,6 +42,7 @@ import kr.co.call.impl.component.MemberChoice import kr.co.call.impl.component.MessageInputField import kr.co.call.impl.component.NameBox import kr.co.call.impl.component.ProfileChoice +import kr.co.call.impl.component.TemporaryProfileImageData import kr.co.call.impl.component.TopTitle import kr.co.call.impl.viewmodel.model.Mbti import kr.co.call.impl.viewmodel.state.Onboarding2State @@ -52,7 +56,6 @@ private enum class Onboarding2EditingNameField { @Composable fun Onboarding2Screen( onBackClick: () -> Unit, - onProfileClick: () -> Unit, onNextClick: (Onboarding2State) -> Unit, modifier: Modifier = Modifier, profileImageUrl: String? = null, @@ -67,6 +70,25 @@ fun Onboarding2Screen( mutableStateOf(null) } var nameInput by rememberSaveable { mutableStateOf("") } + //바텀시트 표시 여부 + var showProfileImagePicker by rememberSaveable { + mutableStateOf(false) + } + //성별 선택 + var selectedGender by rememberSaveable { + mutableStateOf(ProfileImageGender.MALE) + } + //캐러셀에서 보고 있는 사진 + var selectedImageId by rememberSaveable { + mutableStateOf(null) + } + //저장 버튼을 눌러 확정한 사진 + var savedProfileImageUrl by rememberSaveable { + mutableStateOf(profileImageUrl) + } + + val currentProfileImages = + TemporaryProfileImageData.imagesFor(selectedGender) val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current @@ -112,9 +134,14 @@ fun Onboarding2Screen( ProfileChoice( state= ProfileChoiceState( - imageUrl = profileImageUrl, + imageUrl = savedProfileImageUrl, ), - onClick = onProfileClick, + onClick = { + if (selectedImageId == null) { + selectedImageId = currentProfileImages.firstOrNull()?.id + } + showProfileImagePicker=true + }, ) Spacer(modifier = Modifier.height(24.dp)) @@ -247,5 +274,43 @@ fun Onboarding2Screen( .padding(horizontal = 16.dp, vertical = 13.dp), ) } + if (showProfileImagePicker) { + ProfileImagePickerBottomSheet( + images = currentProfileImages, + selectedGender = selectedGender, + selectedImageId = selectedImageId, + + onGenderChange = { newGender -> + selectedGender = newGender + + selectedImageId = TemporaryProfileImageData + .imagesFor(newGender) + .firstOrNull() + ?.id + }, + + onImageSelected = { image -> + selectedImageId = image.id + }, + + onSaveClick = { + savedProfileImageUrl = currentProfileImages + .firstOrNull { image -> + image.id == selectedImageId + } + ?.imageUrl + + showProfileImagePicker = false + }, + + onDismissRequest = { + showProfileImagePicker = false + }, + + title = "원하는 사진을\n선택해주세요", + confirmText = "저장하기", + ) + } } } + From 6a1a790bb8aec053daf456adac90bc7d15cd14a6 Mon Sep 17 00:00:00 2001 From: dada4 Date: Fri, 31 Jul 2026 13:44:05 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=EC=98=A8=EB=B3=B4=EB=94=A94=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=88=98=EC=A0=95=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/call/impl/screen/Onboarding4Screen.kt | 136 +++++++++--------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt index 85b3e256..2737ba6b 100644 --- a/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -28,6 +28,18 @@ import kr.co.call.impl.component.KeywordChoice import kr.co.call.impl.component.TopTitle import kr.co.call.impl.viewmodel.model.Trait +private val traitRows=listOf( + listOf(Trait.HUMOROUS, Trait.PLAYFUL), + listOf(Trait.AFFECTIONATE, Trait.JEALOUS,Trait.TALKATIVE), + listOf(Trait.DAD_JOKE_LOVER, Trait.HOMEBODY), + listOf(Trait.TEASING, Trait.POSSESSIVE, Trait.TSUNDERE), + listOf(Trait.EXPRESSIVE, Trait.PET_NAME_LOVER), + listOf(Trait.EXCLUSIVE, Trait.QUIRKY, Trait.LAID_BACK), + listOf(Trait.OPENLY_JEALOUS, Trait.SHY), + listOf(Trait.SMOOTH_TALKER, Trait.FREQUENT_CHECKER), + listOf(Trait.GOOD_LISTENER, Trait.COMPLIMENTER), +) + @OptIn(ExperimentalLayoutApi::class) @Composable fun Onboarding4Screen( @@ -56,86 +68,74 @@ fun Onboarding4Screen( totalStep = 4, horizontalPadding = 27.dp, ) - Spacer(modifier=Modifier.height(5.dp)) + Spacer(modifier = Modifier.height(5.dp)) Column( - modifier = Modifier + modifier=Modifier .weight(1f) .fillMaxWidth() .background(SubGray2) .navigationBarsPadding(), ) { - FlowRow( - modifier = Modifier.padding( - start = 27.dp, - end = 27.dp, - top = 20.dp, + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 23.dp, ), - horizontalArrangement = Arrangement.spacedBy(20.dp), - verticalArrangement = Arrangement.spacedBy(22.dp), - maxItemsInEachRow = 3, - ) { - Trait.entries.forEach { trait -> - val selectedOrder = selectedKeywords - .indexOf(trait.keyword) - .takeIf { it >= 0 } - ?.plus(1) + verticalArrangement = Arrangement.spacedBy(22.dp), + ) { + traitRows.forEach { rowTraits -> + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(22.dp), + ) { + rowTraits.forEach { trait -> + val selectedOrder = selectedKeywords + .indexOf(trait.keyword) + .takeIf { it >= 0 } + ?.plus(1) - KeywordChoice( - emoji = trait.emoji, - text = trait.label, - selectedOrder = selectedOrder, - onClick = { - selectedKeywords = when { - trait.keyword in selectedKeywords -> - selectedKeywords - trait.keyword + KeywordChoice( + emoji = trait.emoji, + text = trait.label, + selectedOrder = selectedOrder, + onClick = { + selectedKeywords = when { + trait.keyword in selectedKeywords -> + selectedKeywords - trait.keyword - selectedKeywords.size < 5 -> - selectedKeywords + trait.keyword + selectedKeywords.size < 5 -> + selectedKeywords + trait.keyword - else -> selectedKeywords - } - }, - ) + else -> selectedKeywords + } + }, + ) + } } } - Spacer(modifier = Modifier.weight(1f)) - SecondaryButton( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 27.dp, - end = 27.dp, - bottom = 18.dp, - ), - text = "다음", - enabled=selectedTraits.isNotEmpty(), - onClick = { if(selectedTraits.isNotEmpty()) { - onNextClick(selectedTraits) - } - }, - ) + } + Spacer(modifier = Modifier.weight(1f)) + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 27.dp, + end = 27.dp, + bottom = 18.dp, + ), + text = "다음", + enabled = selectedTraits.isNotEmpty(), + onClick = { + if (selectedTraits.isNotEmpty()) { + onNextClick(selectedTraits) + } + }, + ) + } } } -} - -@Preview( - name = "Onboarding 4 - 선택됨", - showBackground = true, - widthDp = 393, - heightDp = 852, -) -@Composable -private fun Onboarding4ScreenPreview() { - CallFromAiTheme { - Onboarding4Screen( - onBackClick = {}, - onNextClick = {}, - initialSelectedKeywords = listOf( - Trait.HUMOROUS.keyword, - Trait.PLAYFUL.keyword, - Trait.AFFECTIONATE.keyword, - ), - ) - } -} \ No newline at end of file