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 f1b3d7ac..0c38192b 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 @@ -20,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.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator @@ -43,8 +43,13 @@ import kr.co.call.api.ManagerChatRoomNayKey import kr.co.call.api.MyPageNavKey import kr.co.call.api.ProfileNavKey import kr.co.call.api.SubscriptionNavKey +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.Onboarding6NavKey import kr.co.call.api.TermNavKey -import kr.co.call.api.OnboardingNavKey import kr.co.call.callfromai.ui.MainBottomBar import kr.co.call.callfromai.ui.MainTab import kr.co.call.callfromai.util.toMainTab @@ -95,7 +100,6 @@ fun AppScreen(modifier: Modifier = Modifier) { val bottomBarPadding = remember(bottomBarHeightPx) { with(density) { bottomBarHeightPx.toDp() } } - Scaffold( modifier = modifier.fillMaxSize(), bottomBar = { @@ -108,10 +112,9 @@ fun AppScreen(modifier: Modifier = Modifier) { ) } }, - contentWindowInsets = WindowInsets.safeDrawing - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - .exclude(WindowInsets.ime), - + contentWindowInsets = WindowInsets.safeDrawing.only( + WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal + ).exclude(WindowInsets.ime), ) { padding -> CompositionLocalProvider( LocalBottomBarPadding provides if (showBottomBar) bottomBarPadding else 0.dp, @@ -126,16 +129,16 @@ fun AppScreen(modifier: Modifier = Modifier) { entryProvider = entryProvider { loginEntry( navigateToLogin={ - appNavigator.navigate(LoginNavKey) + appNavigator.replaceAll(LoginNavKey) }, navigateToOnboarding = { - appNavigator.navigate(OnboardingNavKey) + appNavigator.replaceAll(Onboarding1NavKey) }, navigateToHome = { - appNavigator.navigate(HomeNavKey) + appNavigator.replaceAll(HomeNavKey) }, navigateToAgreement={ - appNavigator.navigate(AgreementNavKey) + appNavigator.replaceAll(AgreementNavKey) }, navigateToAgreementDetail={term -> appNavigator.navigate( @@ -147,13 +150,48 @@ fun AppScreen(modifier: Modifier = Modifier) { ) }, navigateAfterAgreement={ - appNavigator.navigate(OnboardingNavKey) + appNavigator.navigate(Onboarding1NavKey) }, onBack={ appNavigator.popBackStack() } ) - onboardingEntry() + onboardingEntry( + 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 = { + appNavigator.navigate(Onboarding6NavKey) + }, + onOnboarding6CallNow = { + //나중에 전화화면으로 바꾸기 + appNavigator.replaceAll(HomeNavKey) + }, + onOnboarding6CallLater = { + appNavigator.replaceAll(HomeNavKey) + }, + ) homeEntry( navigateToCall = { characterId -> appNavigator.navigate( @@ -163,7 +201,9 @@ fun AppScreen(modifier: Modifier = Modifier) { ) }, navigateToCallRecord = { callId -> - appNavigator.navigate(CallRecordNavKey(callId = callId)) + appNavigator.navigate( + CallRecordNavKey(callId = callId), + ) }, onCallRecordBack = { appNavigator.popBackStack() 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 547b0234..ec58b68d 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 @@ -24,6 +24,9 @@ val SubGray2 = Color(0xFFF7F3F3) val SubRed = Color(0xFFFF0000) val SubBlue = Color(0xFF4391FF) +val SubFire=Color(0xFFFF5677) +val SubSoup=Color(0xFFFFCE91) + val SubYellow=Color(0xFFFDE500) val SubPressed = Color(0xFFED5988) @@ -81,6 +84,13 @@ val ChatGradientReverse = 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 27610f05..bd9b8357 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 @@ -59,6 +59,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 captionSmallBold: TextStyle = pretendard(8, 1.5f, FontWeight.SemiBold), // 신규 ) 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..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 @@ -4,4 +4,21 @@ 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: NavKey + +@Serializable +data object Onboarding5NavKey: NavKey + +@Serializable +data object Onboarding6NavKey: NavKey + 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/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/BackStepBar.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt new file mode 100644 index 00000000..dde623d3 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BackStepBar.kt @@ -0,0 +1,60 @@ +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.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 BackStepBar( + 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() { + 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 new file mode 100644 index 00000000..e0572335 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BirthdayChoice.kt @@ -0,0 +1,136 @@ +package kr.co.call.impl.component + +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 = if (selectedDate == null) Gray600 else 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 -> + if(newDate != null) { + 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 = null, + onDateSelected = {}, + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) + } +} 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..eb8b8b03 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/BottomWheel.kt @@ -0,0 +1,184 @@ +package kr.co.call.impl.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.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 +import androidx.compose.ui.Modifier +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.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, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +){ + val years = remember { + (1900..LocalDate.now().year).toList() + } + + val initialDate = selectedDate ?: LocalDate.now() + + var draftDate by remember(selectedDate) { + mutableStateOf(initialDate) + } + + val yearState = rememberLazyListState( + initialFirstVisibleItemIndex = years + .indexOf(initialDate.year) + .coerceAtLeast(0), + ) + val monthState = rememberLazyListState( + initialFirstVisibleItemIndex = initialDate.monthValue - 1, + ) + val dayState = rememberLazyListState( + 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 = { + val confirmedDate = when { + selectedDate != null -> draftDate + hasUserChangedDate -> draftDate + else -> null + } + onConfirmClick(confirmedDate) + }, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(195.dp), + ) { + HorizontalDivider( + modifier = Modifier + .align(Alignment.Center) + .offset(y = (-22).dp) + .padding(horizontal = 35.dp), + color = CallTheme.colors.gray200, + ) + + HorizontalDivider( + modifier = Modifier + .align(Alignment.Center) + .offset(y = 22.dp) + .padding(horizontal = 35.dp), + color = CallTheme.colors.gray200, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 35.dp), + ) { + WheelPicker( + items = years, + listState = yearState, + itemText = { "${it}년" }, + onItemSelected = { _, year -> + draftDate = LocalDate.of( + year, + draftDate.monthValue, + draftDate.dayOfMonth.coerceAtMost( + YearMonth.of( + year, + draftDate.monthValue, + ).lengthOfMonth(), + ), + ) + }, + modifier = Modifier.weight(1f), + ) + + WheelPicker( + items = (1..12).toList(), + listState = monthState, + itemText = { "%02d월".format(it) }, + onItemSelected = { _, month -> + draftDate = LocalDate.of( + draftDate.year, + month, + draftDate.dayOfMonth.coerceAtMost( + YearMonth.of( + draftDate.year, + month, + ).lengthOfMonth(), + ), + ) + }, + modifier = Modifier.weight(1f), + ) + + WheelPicker( + items = ( + 1..YearMonth + .of( + draftDate.year, + draftDate.monthValue, + ) + .lengthOfMonth() + ).toList(), + listState = dayState, + itemText = { "%02d일".format(it) }, + onItemSelected = { _, day -> + draftDate = draftDate.withDayOfMonth(day) + }, + modifier = Modifier.weight(1f), + ) + } + } + } +} 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..751f2a76 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/KeywordChoice.kt @@ -0,0 +1,153 @@ +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.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.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.White +import kr.co.call.impl.viewmodel.model.Trait + +@Composable +fun KeywordChoice( + emoji: String, + text: String, + selectedOrder: Int?, + onClick:()->Unit, + modifier: Modifier =Modifier, +){ + Box( + modifier=modifier, + ){ + Surface( + modifier=Modifier + .height(33.dp) + .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/MemberChoice.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt new file mode 100644 index 00000000..65eb63fa --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MemberChoice.kt @@ -0,0 +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( + label: String, + selectedOption: String, //드롭다운이 보여줄 값 + placeholder: String, + options:List, //선택 가능한 목록 + onOptionSelected: (String)->Unit,//선택됐을 때 실행할 동작 + required: Boolean=false, + modifier: Modifier=Modifier, + ){ + 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(51.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(114.dp) + .heightIn(max=420.dp), + offset=DpOffset( + x=menuWidth-114.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/MessageInputField.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt new file mode 100644 index 00000000..9744d889 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/MessageInputField.kt @@ -0,0 +1,208 @@ +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.LaunchedEffect +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 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 +fun MessageInputField( + value: String, + onValueChange: (String) -> Unit, + 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 + .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) + .focusRequester(focusRequester), + 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/NameBox.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt new file mode 100644 index 00000000..12fec4b0 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/NameBox.kt @@ -0,0 +1,86 @@ +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 +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.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( + label: String, + value: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + ){ + Column( + modifier=modifier + .width(125.dp) + ){ + Row { + Text(text=label, + style=CallTheme.typography.bodyMediumMedium, + color= Gray600, + ) + Text(text=" *", + style=CallTheme.typography.bodyMediumMedium, + color= SubRed, + ) + } + Spacer(modifier=Modifier.height(6.dp)) + Box( + modifier= Modifier + .fillMaxWidth() + .height(51.dp) + .background( + color=Gray100, + shape=RoundedCornerShape(10.dp) + ) + .clickable(onClick=onClick) + .padding(horizontal=16.dp), + contentAlignment= Alignment.CenterStart + ) { + Text( + text = value.ifBlank { " " }, + color = if (value.isBlank()) Gray400 else Gray900, + style = CallTheme.typography.bodyMediumMedium, + ) + } + } +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF, +) +@Composable +private fun NameBoxPreview() { + NameBox( + label = "이름", + value = "수현", + onClick = {}, + ) +} \ 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..95c86db2 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/PreferTime.kt @@ -0,0 +1,209 @@ +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, + 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 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 new file mode 100644 index 00000000..eac827e7 --- /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.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 +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 +import androidx.compose.runtime.getValue +import kr.co.call.impl.viewmodel.state.ProfileChoiceState + + +@Composable +fun ProfileChoice( + state: ProfileChoiceState, + onClick: ()->Unit, + modifier: Modifier=Modifier, +){ + val interactionSource=remember{MutableInteractionSource()} + val isPressed by interactionSource.collectIsPressedAsState() + + Box( + modifier=modifier + .size(state.size) + .clickable( + interactionSource=interactionSource, + indication = null, + onClick=onClick, ) + ) { + if (state.imageUrl.isNullOrEmpty()){ + Image( + imageVector= ImageVector.vectorResource( + id= if(isPressed){ + R.drawable.onboarding_profile2 + }else {R.drawable.onboarding_profile}, + ), + contentDescription="기본 프로필 이미지", + modifier=Modifier.fillMaxSize(), + contentScale= ContentScale.Fit + ) + } else{ + AsyncImage( + model=state.imageUrl, + contentDescription="선택된 프로필 사진", + modifier=Modifier + .fillMaxSize() + .clip(CircleShape), + contentScale=ContentScale.Crop + ) + } + } +} 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..ba004e1b --- /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 = 110.dp, + height = 172.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=6.dp, + vertical=13.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.bodyMediumBold.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/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..84162a49 --- /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 ( + onTemperatureChange: (Int)->Unit, + modifier: Modifier= Modifier, +) { + 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/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/component/TopTitle.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt new file mode 100644 index 00000000..d2a6725b --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/component/TopTitle.kt @@ -0,0 +1,168 @@ +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 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( + modifier: Modifier= Modifier, + title: String, + description: String, + currentStep: Int?=null, + totalStep: Int?=null, + horizontalPadding: Dp =14.dp, + ) { + val progress = if ( + currentStep != null && totalStep != null && totalStep > 0 + ) { + (currentStep.toFloat() / totalStep.toFloat()).coerceIn(0f, 1f) + } else { + null + } + + Column( + modifier = modifier.fillMaxWidth(), + ) { + if (progress != null) { + Box( + modifier= Modifier + .fillMaxWidth() + .height(4.dp) + .background(Gray100) + ){ + Box( + modifier= Modifier + .fillMaxWidth(progress) + .clip( + RoundedCornerShape( + topEnd = 100.dp, + bottomEnd = 100.dp), + ) + .fillMaxHeight() + .background(brush= MainGradient), + ) + } + Spacer( + modifier = Modifier.height(23.dp), + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start=horizontalPadding, + end = horizontalPadding), + verticalAlignment = Alignment.Top, + ) { + Text( + text = title, + style = CallTheme.typography.titleMediumBold, + color = Black, + modifier = Modifier.weight(1f), + ) + + 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(5.dp)) + Text( + text = description, + modifier=Modifier.padding(start=horizontalPadding), + 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 +private fun TopTitleStep2Preview() { + CallFromAiTheme { + TopTitle( + title = "어떤 사이로\n시작할까요?", + description = "원하는 상대방과의 관계를 선택해주세요.", + currentStep = 2, + totalStep = 4, + ) + } +} + +@Preview( + name = "3단계", + showBackground = true, +) +@Composable +private fun TopTitleStep3Preview() { + CallFromAiTheme { + TopTitle( + title = "어떤 매력에\n끌리나요?", + description = "우선 순위에 따라 최대 5개를 선택해주세요.", + currentStep = 3, + totalStep = 4, + ) + } +} + +@Preview( + name = "4단계", + showBackground = true, +) +@Composable +private fun TopTitleStep4Preview() { + CallFromAiTheme { + TopTitle( + 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/entry/OnboardingEntryBuilder.kt b/feature/onboarding/impl/src/main/java/kr/co/call/impl/entry/OnboardingEntryBuilder.kt index d122074b..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 @@ -1,12 +1,128 @@ 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 -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.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 +import kr.co.call.impl.screen.Onboarding4Screen +import kr.co.call.impl.screen.Onboarding5Screen +import kr.co.call.impl.screen.Onboarding6Screen +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.runtime.getValue +import kr.co.call.impl.viewmodel.OnboardingViewModel -fun EntryProviderScope.onboardingEntry() { - entry { - OnboardingScreen() +fun EntryProviderScope.onboardingEntry( + onOnboarding1Next: () -> Unit, + onBackFromOnboarding2: () -> Unit, + onOnboarding2Next: () -> Unit, + onBackFromOnboarding3: () -> Unit, + onOnboarding3Next: () -> Unit, + onBackFromOnboarding4: () -> Unit, + onOnboarding4Next: () -> Unit, + onBackFromOnboarding5: () -> Unit, + onOnboarding5Next: () -> Unit, + onOnboarding6CallNow: () -> Unit, + onOnboarding6CallLater: () -> Unit, +) { + entry { + val onboardingViewModel = sharedOnboardingViewModel() + Onboarding1Screen( + onNextClick = {state -> + onboardingViewModel.updateUserProfile( + lastName = state.lastName, + firstName = state.firstName, + birthday = state.birthday, + job = state.job, + mbti = state.mbti, + ) + onOnboarding1Next() + }, + ) } + + entry { + val onboardingViewModel = sharedOnboardingViewModel() + Onboarding2Screen( + onBackClick = onBackFromOnboarding2, + onNextClick ={ state -> + onboardingViewModel.updateAiProfile( + age = state.age, + lastName = state.lastName, + firstName = state.firstName, + job = state.job, + mbti = state.mbti, + ) + + onOnboarding2Next() + }, + ) + } + + entry { + val onboardingViewModel = sharedOnboardingViewModel() + Onboarding3Screen( + onBackClick = onBackFromOnboarding3, + onNextClick = { speechStyle, relationship, temperature -> + onboardingViewModel.updateConversationStyle( + speechStyle = speechStyle, + relationship = relationship, + temperature = temperature, + ) + onOnboarding3Next() + }, + ) + } + + entry { + val onboardingViewModel = sharedOnboardingViewModel() + Onboarding4Screen( + onBackClick = onBackFromOnboarding4, + onNextClick = { traits -> + onboardingViewModel.updateTraits(traits) + onOnboarding4Next() + }, + ) + } + + entry { + val onboardingViewModel = sharedOnboardingViewModel() + Onboarding5Screen( + onBackClick = onBackFromOnboarding5, + onNextClick = { preferTime -> + onboardingViewModel.updatePreferTime(preferTime) + onOnboarding5Next() + }, + ) + } + + entry { + val onboardingViewModel = sharedOnboardingViewModel() + val uiState by onboardingViewModel.container.stateFlow + .collectAsStateWithLifecycle() + Onboarding6Screen( + firstName = uiState.aiFirstName, + onCallNowClick = onOnboarding6CallNow, + onCallLaterClick = onOnboarding6CallLater, + ) + } +} + +@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 new file mode 100644 index 00000000..36214009 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding1Screen.kt @@ -0,0 +1,312 @@ +package kr.co.call.impl.screen + +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 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 +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.TemporaryProfileImageData +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, + FIRST_NAME, +} +@Composable +fun Onboarding1Screen ( + onNextClick: (Onboarding1State)->Unit, + modifier: Modifier =Modifier, + profileImageUrl: String?=null, +) { + var lastName by rememberSaveable { + mutableStateOf("") + } + var firstName by rememberSaveable { + mutableStateOf("") + } + var birthday by remember { + mutableStateOf(null) + } + var job by rememberSaveable { + mutableStateOf("") + } + var mbti by rememberSaveable { + mutableStateOf("") + } + var editingNameField by rememberSaveable { + 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 canMoveNext = + lastName.isNotBlank() && + firstName.isNotBlank() && + birthday != null && + job.isNotBlank() + Box( + modifier = modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding() + .padding( + start = 27.dp, + end = 27.dp, + top = 54.dp, + bottom = 18.dp, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + TopTitle( + title = "먼저 나를\n소개해볼까요?", + description = "전화왔어에서 사용할 나의 프로필을 완성해주세요.", + horizontalPadding = 0.dp, + ) + Spacer(modifier = Modifier.height(23.dp)) + Text( + text="사진선택", + style= CallTheme.typography.bodyMedium, + color= Gray600, + ) + Spacer(modifier = Modifier.height(18.dp)) + ProfileChoice( + state= ProfileChoiceState( + imageUrl = savedProfileImageUrl, + size=99.dp, + ), + onClick ={ + if (selectedImageId==null){ + selectedImageId=currentProfileImages.firstOrNull()?.id + } + showProfileImagePicker=true + }, + ) + 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 = Mbti.entries.map { it.name }, + onOptionSelected = { mbti = it }, + ) + } + Column( + modifier=Modifier.fillMaxWidth(), + ) { + 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 = { + birthday?.let { selectedBirthday -> + onNextClick( + Onboarding1State( + lastName=lastName, + firstName=firstName, + birthday=selectedBirthday, + job=job, + mbti=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, + ), + ) + } + 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 new file mode 100644 index 00000000..612fb4bd --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding2Screen.kt @@ -0,0 +1,316 @@ +package kr.co.call.impl.screen + +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.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 +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.BackStepBar +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 +import kr.co.call.impl.viewmodel.state.ProfileChoiceState + + +private enum class Onboarding2EditingNameField { + LAST_NAME, + FIRST_NAME, +} +@Composable +fun Onboarding2Screen( + onBackClick: () -> Unit, + onNextClick: (Onboarding2State) -> 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("") } + //바텀시트 표시 여부 + 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 + + val canMoveNext = age.isNotBlank() && + lastName.isNotBlank() && + firstName.isNotBlank() && + job.isNotBlank() + + Box( + modifier = modifier + .fillMaxSize() + .background(White), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + ) { + BackStepBar(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( + state= ProfileChoiceState( + imageUrl = savedProfileImageUrl, + ), + onClick = { + if (selectedImageId == null) { + selectedImageId = currentProfileImages.firstOrNull()?.id + } + showProfileImagePicker=true + }, + ) + + 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 = Mbti.entries.map { it.name }, + 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( + Onboarding2State( + age=age, + lastName=lastName, + firstName=firstName, + job=job, + mbti=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), + ) + } + 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 = "저장하기", + ) + } + } +} + 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..6dc07431 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding3Screen.kt @@ -0,0 +1,211 @@ +package kr.co.call.impl.screen + +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.Alignment +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.BackStepBar +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 + + +@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(), + ) { + BackStepBar( + onBackClick = onBackClick, + ) + TopTitle( + title = "어떤 사이로\n시작할까요?", + description = "원하는 상대방과의 관계를 선택해주세요.", + currentStep = 2, + totalStep = 4, + horizontalPadding = 27.dp, + ) + Spacer(modifier=Modifier.height(5.dp)) + + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(SubGray2), + ) { + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 27.dp), + ) { + 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.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 }, + ) + } + 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 new file mode 100644 index 00000000..2737ba6b --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding4Screen.kt @@ -0,0 +1,141 @@ +package kr.co.call.impl.screen + +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 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.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.BackStepBar +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( + 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), + ) { + BackStepBar(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(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 23.dp, + ), + 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 + + 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 = "다음", + enabled = selectedTraits.isNotEmpty(), + onClick = { + if (selectedTraits.isNotEmpty()) { + onNextClick(selectedTraits) + } + }, + ) + } + } + } 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..0e2bb017 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding5Screen.kt @@ -0,0 +1,111 @@ +package kr.co.call.impl.screen + +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.BackStepBar +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? = null, + modifier: Modifier = Modifier, +){ + var selectedPreferTime by rememberSaveable { + mutableStateOf(initialPreferTime) + } + Column( + modifier = modifier + .fillMaxSize() + .background(White), + ) { + BackStepBar(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 = "다음", + enabled=selectedPreferTime != null, + onClick = { selectedPreferTime?.let(onNextClick) }, + ) + } + } +} + +@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..4fab5bb9 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/screen/Onboarding6Screen.kt @@ -0,0 +1,211 @@ +package kr.co.call.impl.screen + +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 isCallDialogVisible 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= { isCallDialogVisible=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 (isCallDialogVisible){ + TwoButtonPopup( + label="통화 연결", + title="${firstName}에게 바로\n통화를 연결할까요?", + positiveText = "연결", + negativeText = "취소", + onPositiveClick = { + isCallDialogVisible=false + onCallNowClick() + }, + onNegativeClick = { + isCallDialogVisible=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/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/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..b24cfd34 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingUiState.kt @@ -0,0 +1,38 @@ +package kr.co.call.impl.viewmodel + +import androidx.compose.runtime.Composable +import kr.co.call.impl.component.PreferTime +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 + +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, + + 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 new file mode 100644 index 00000000..8b1628c7 --- /dev/null +++ b/feature/onboarding/impl/src/main/java/kr/co/call/impl/viewmodel/OnboardingViewModel.kt @@ -0,0 +1,99 @@ +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.viewmodel.model.Trait +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 +import javax.inject.Inject + +@HiltViewModel +class OnboardingViewModel @Inject constructor() : ViewModel(), + ContainerHost { + override val container=container( + initialState= OnboardingUiState(), + ) + + fun updateUserProfile( + lastName: String, + firstName: String, + birthday: LocalDate, + job: String, + mbti: String, + ) =intent{ + reduce{ + state.copy( + userLastName = lastName, + userFirstName = firstName, + userBirthday = birthday, + userJob = job, + userMbti = mbti, + ) + } + } + + fun updateAiProfile( + age: String, + lastName: String, + firstName: String, + job: String, + mbti: String, + )=intent { + reduce { + state.copy( + aiFirstName = firstName, + aiLastName = lastName, + aiAge = age, + aiJob = job, + aiMbti = mbti, + ) + } + } + + + fun updateConversationStyle( + speechStyle: SpeechStyle, + relationship: Relationship, + temperature: Int, + ) =intent { + reduce { + state.copy( + speechStyle = speechStyle, + relationship = relationship, + temperature = temperature, + ) + } + } + + + fun updateTraits(traits: List + )=intent{ + reduce { + state.copy(traits = traits) + } + } + + // + 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 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/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 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 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 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 @@ + + + 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 @@ + + + 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 00000000..2ceac5b9 Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/ic_down.png differ 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/ic_up.png b/feature/onboarding/impl/src/main/res/drawable/ic_up.png new file mode 100644 index 00000000..03980cc3 Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/ic_up.png differ 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 00000000..40a1644b Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/onboarding_check.png differ diff --git a/feature/onboarding/impl/src/main/res/drawable/onboarding_profile.xml b/feature/onboarding/impl/src/main/res/drawable/onboarding_profile.xml new file mode 100644 index 00000000..a1c16976 --- /dev/null +++ b/feature/onboarding/impl/src/main/res/drawable/onboarding_profile.xml @@ -0,0 +1,30 @@ + + + + + + + + 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/relationship_first.png b/feature/onboarding/impl/src/main/res/drawable/relationship_first.png new file mode 100644 index 00000000..445a3a2f Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/relationship_first.png differ diff --git a/feature/onboarding/impl/src/main/res/drawable/relationship_old.png b/feature/onboarding/impl/src/main/res/drawable/relationship_old.png new file mode 100644 index 00000000..f9680e2e Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/relationship_old.png differ diff --git a/feature/onboarding/impl/src/main/res/drawable/relationship_some.png b/feature/onboarding/impl/src/main/res/drawable/relationship_some.png new file mode 100644 index 00000000..df3acc95 Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/relationship_some.png differ 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 00000000..b4f7e7ad Binary files /dev/null and b/feature/onboarding/impl/src/main/res/drawable/spendbutton.png differ