diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6df0e3f..f58a003 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -57,11 +57,13 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) + implementation(compose.material) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(compose.materialIconsExtended) // Navigation implementation(libs.navigation.compose) // Ktor 핵심 클라이언트 diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_back.png b/composeApp/src/commonMain/composeResources/drawable/ic_back.png new file mode 100644 index 0000000..bc73586 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_back.png differ diff --git a/composeApp/src/commonMain/composeResources/drawable/img_logo_orange.png b/composeApp/src/commonMain/composeResources/drawable/img_logo_orange.png new file mode 100644 index 0000000..06002f6 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/img_logo_orange.png differ diff --git a/composeApp/src/commonMain/composeResources/drawable/img_logo_white.png b/composeApp/src/commonMain/composeResources/drawable/img_logo_white.png new file mode 100644 index 0000000..bac455f Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/img_logo_white.png differ diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 6902b22..b86f717 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1,4 +1,45 @@ + + 로그인 + E-mail + password + 이메일을 입력해주세요 + 비밀번호를 입력해주세요 + 로그인 + 회원가입 + 비밀번호 찾기 + + + 이메일을 입력해주세요. + 다음 + 뒤로가기 + + + 비밀번호 찾기 + 이메일 전송 + + + 인증번호를 입력해주세요. + 확인 + + + 비밀번호를 입력해주세요. + 비밀번호 입력 + 비밀번호 확인 + 비밀번호 입력 (8~20자 영문, 숫자) + 비밀번호 확인 (8~20자 영문, 숫자) + + + 반가워요! + 닉네임을 입력해주세요. + 닉네임을 입력해주세요 + + + 동아리/학과의 + 고유 번호를 입력해주세요. + 고유 번호가 일치하지 않습니다. + 확인하기 + 지금 %1$s의 재실자는? diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/App.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/App.kt index 0d6240b..6d4249c 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/App.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/App.kt @@ -2,14 +2,12 @@ package org.whosin.client import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController import org.jetbrains.compose.ui.tooling.preview.Preview import org.whosin.client.core.navigation.WhosInNavGraph import ui.theme.WhosInTheme -import whosinclient.composeapp.generated.resources.Res -import whosinclient.composeapp.generated.resources.compose_multiplatform @Composable diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt index 0a716ff..d530d65 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/Route.kt @@ -7,9 +7,30 @@ sealed interface Route { @Serializable data object AuthGraph: Route + @Serializable + data object Splash: Route + @Serializable data object Login: Route + @Serializable + data object FindPassword: Route + + @Serializable + data object Signup: Route + + @Serializable + data object EmailVerification: Route + + @Serializable + data object PasswordInput: Route + + @Serializable + data object NicknameInput: Route + + @Serializable + data object ClubCodeInput: Route + /* 메인 화면 */ @Serializable data object Home: Route diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt index 956f52a..5a9d3ce 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/core/navigation/WhosInNavGraph.kt @@ -6,7 +6,14 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navigation -import org.whosin.client.presentation.auth.LoginScreen +import org.whosin.client.presentation.auth.clubcode.ClubCodeInputScreen +import org.whosin.client.presentation.auth.login.EmailVerificationScreen +import org.whosin.client.presentation.auth.login.LoginScreen +import org.whosin.client.presentation.auth.login.NicknameInputScreen +import org.whosin.client.presentation.auth.login.FindPasswordScreen +import org.whosin.client.presentation.auth.login.PasswordInputScreen +import org.whosin.client.presentation.auth.login.SignupScreen +import org.whosin.client.presentation.auth.login.SplashScreen import org.whosin.client.presentation.home.HomeScreen import org.whosin.client.presentation.mypage.MyPageScreen @@ -21,13 +28,95 @@ fun WhosInNavGraph( ) { /* 인증,인가 화면들을 위한 별도의 graph */ navigation( - startDestination = Route.Login, + startDestination = Route.Splash, ) { + composable { + SplashScreen( + modifier = modifier, + onNavigateToLogin = { + navController.navigate(Route.Login) { + popUpTo(Route.Splash) { inclusive = true } + } + } + ) + } + composable { LoginScreen( modifier = modifier, onNavigateToHome = { navController.navigate(Route.Home) + }, + onNavigateToFindPassword = { + navController.navigate(Route.FindPassword) + }, + onNavigateToSignup = { + navController.navigate(Route.Signup) + } + ) + } + + composable { + FindPasswordScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onPasswordResetComplete = { + navController.navigate(Route.Login) { + popUpTo(Route.FindPassword) { inclusive = true } + } + } + ) + } + + composable { + SignupScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onNavigateToEmailVerification = { email -> + navController.navigate(Route.EmailVerification) + } + ) + } + + composable { backStackEntry -> + + EmailVerificationScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onVerificationComplete = { + navController.navigate(Route.PasswordInput) + } + ) + } + + composable { + PasswordInputScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onPasswordComplete = { password, confirmPassword -> + navController.navigate(Route.NicknameInput) + } + ) + } + + composable { + NicknameInputScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onNavigateToClubCode = { + navController.navigate(Route.ClubCodeInput) + } + ) + } + + composable { + ClubCodeInputScreen( + modifier = modifier, + onNavigateBack = { navController.navigateUp() }, + onNavigateToHome = { + navController.navigate(Route.Home) { + popUpTo(Route.AuthGraph) { inclusive = true } + } } ) } diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt index 660d86e..ed87075 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/di/DIModules.kt @@ -11,7 +11,7 @@ import org.whosin.client.data.repository.DummyRepository import org.whosin.client.data.repository.ClubRepository import org.whosin.client.data.repository.MemberRepository import org.whosin.client.presentation.dummy.DummyViewModel -import org.whosin.client.presentation.auth.LoginViewModel +import org.whosin.client.presentation.auth.login.viewmodel.LoginViewModel import org.whosin.client.presentation.home.HomeViewModel import org.whosin.client.presentation.mypage.MyPageViewModel diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt deleted file mode 100644 index cb21cf2..0000000 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginScreen.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.whosin.client.presentation.auth - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import org.jetbrains.compose.ui.tooling.preview.Preview - -@Composable -fun LoginScreen( - modifier: Modifier = Modifier, - onNavigateToHome: () -> Unit -) { - Box( - modifier = modifier - ) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "Login Screen" - ) - Button( - onClick = onNavigateToHome - ){ - Text(text = "Go to Home") - } - } - } -} - -@Preview -@Composable -fun LoginScreenPreview() { - LoginScreen( - modifier = Modifier.fillMaxSize(), - onNavigateToHome = {} - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt new file mode 100644 index 0000000..65ffc7f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeInputScreen.kt @@ -0,0 +1,294 @@ +package org.whosin.client.presentation.auth.clubcode + +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.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.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.NumberInputBox +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.club_code_confirm_button +import whosinclient.composeapp.generated.resources.club_code_error_message +import whosinclient.composeapp.generated.resources.club_code_title_1 +import whosinclient.composeapp.generated.resources.club_code_title_2 +import whosinclient.composeapp.generated.resources.confirm_button +import whosinclient.composeapp.generated.resources.ic_back + + +@Composable +fun ClubCodeInputScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onNavigateToHome: (String) -> Unit = {}, + onVerifyClubCode: (String) -> Unit = {}, + onErrorReset: () -> Unit = {}, + verificationState: ClubCodeState = ClubCodeState.INPUT, + clubName: String = "" +) { + var clubCode by remember { mutableStateOf(arrayOf("", "", "", "", "", "")) } + var currentFocusIndex by remember { mutableStateOf(0) } + val currentState = verificationState + val focusRequesters = remember { List(6) { FocusRequester() } } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + delay(300) + focusRequesters[0].requestFocus() + keyboardController?.show() + } + + // 화면 종료 시 키보드 숨기기 + DisposableEffect(Unit) { + onDispose { + keyboardController?.hide() + } + } + + LaunchedEffect(currentState) { + if (currentState == ClubCodeState.ERROR) { + delay(1000) + clubCode = arrayOf("", "", "", "", "", "") + currentFocusIndex = 0 + focusRequesters[0].requestFocus() + onErrorReset() + } + } + + val fullCode = clubCode.joinToString("") + val isComplete = fullCode.length == 6 + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier.size(18.dp) + ) + } + + // 제목 + Text( + text = stringResource(Res.string.club_code_title_1), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 4.dp) + ) + + Text( + text = stringResource(Res.string.club_code_title_2), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 68.dp) + ) + + // 6자리 번호 입력 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + repeat(6) { index -> + NumberInputBox( + value = clubCode[index], + onValueChange = { input -> + if (input.length <= 1 && input.all { it.isDigit() }) { + val newCode = clubCode.copyOf() + val wasEmpty = clubCode[index].isEmpty() + newCode[index] = input + clubCode = newCode + + if (input.isNotEmpty() && index < 5) { + currentFocusIndex = index + 1 + focusRequesters[index + 1].requestFocus() + } else if (input.isEmpty() && !wasEmpty && index > 0) { + currentFocusIndex = index - 1 + focusRequesters[index - 1].requestFocus() + } + } + }, + onBackspace = { + if (index > 0) { + val prevIndex = index - 1 + currentFocusIndex = prevIndex + focusRequesters[prevIndex].requestFocus() + } + }, + onFocusChanged = { isFocused -> + if (isFocused) { + currentFocusIndex = index + } + }, + borderColor = when (currentState) { + ClubCodeState.ERROR -> Color(0xFFFF3636) + else -> Color(0xFFE5E5E5) + }, + focusedBorderColor = when (currentState) { + ClubCodeState.ERROR -> Color(0xFFFF3636) + else -> Color(0xFFF89531) + }, + isFocused = currentFocusIndex == index, + modifier = Modifier + .weight(1f) + .focusRequester(focusRequesters[index]) + ) + } + } + + // 에러 메시지 + if (currentState == ClubCodeState.ERROR) { + Text( + text = stringResource(Res.string.club_code_error_message), + fontSize = 16.sp, + fontWeight = FontWeight.W500, + color = Color(0xFFFF3636), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 12.dp, bottom = 12.dp) + .fillMaxWidth() + ) + } + + Box( + modifier = Modifier + .padding(top = if (currentState != ClubCodeState.ERROR) 48.dp else 0.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + CommonLoginButton( + text = stringResource(Res.string.club_code_confirm_button), + onClick = { + if (isComplete) { + onVerifyClubCode(fullCode) + } + }, + enabled = isComplete, + modifier = Modifier + .size(width = 108.dp, height = 44.dp) + ) + } + + + if (currentState == ClubCodeState.SUCCESS) { + // 동아리 정보 박스 + Box( + modifier = Modifier + .padding(top = 56.dp) + .padding(horizontal = 40.dp) + .fillMaxWidth() + .height(88.dp) + .background( + color = Color(0xFFFBFBFB), + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + color = Color(0xFFE5E5E5), + shape = RoundedCornerShape(8.dp) + ), + + contentAlignment = Alignment.Center + ) { + Text( + text = clubName, + fontSize = 16.sp, + fontWeight = FontWeight.W500, + color = Color.Black, + textAlign = TextAlign.Center + ) + } + } + } + + // 하단 확인 버튼 + CommonLoginButton( + text = stringResource(Res.string.confirm_button), + onClick = { + if (currentState == ClubCodeState.SUCCESS) { + onNavigateToHome(clubName) + } + }, + enabled = currentState == ClubCodeState.SUCCESS, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + +@Preview +@Composable +fun ClubCodeInputScreenPreview() { + var verificationState by remember { mutableStateOf(ClubCodeState.INPUT) } + var clubName by remember { mutableStateOf("") } + + ClubCodeInputScreen( + modifier = Modifier, + verificationState = verificationState, + clubName = clubName, + onNavigateBack = {}, + onNavigateToHome = { name -> println("Navigate to home with: $name") }, + onVerifyClubCode = { code -> + if (code == "123456") { + verificationState = ClubCodeState.SUCCESS + clubName = "메이커스팜" + } else { + verificationState = ClubCodeState.ERROR + } + }, + onErrorReset = { + verificationState = ClubCodeState.INPUT + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeState.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeState.kt new file mode 100644 index 0000000..3dc43ce --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/clubcode/ClubCodeState.kt @@ -0,0 +1,7 @@ +package org.whosin.client.presentation.auth.clubcode + +enum class ClubCodeState { + INPUT, // 입력 중 + SUCCESS, // 성공 (동아리 정보 표시) + ERROR // 실패 (에러 메시지 표시) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt new file mode 100644 index 0000000..4f5380e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/EmailVerificationScreen.kt @@ -0,0 +1,174 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.NumberInputBox +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.confirm_button +import whosinclient.composeapp.generated.resources.email_verification_title +import whosinclient.composeapp.generated.resources.ic_back + +@Composable +fun EmailVerificationScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onVerificationComplete: (String) -> Unit = {} +) { + var verificationCode by remember { mutableStateOf(arrayOf("", "", "", "", "", "")) } + var currentFocusIndex by remember { mutableStateOf(0) } + val focusRequesters = remember { List(6) { FocusRequester() } } + val keyboardController = LocalSoftwareKeyboardController.current + + // 화면 진입 시 첫 번째 입력 박스에 포커스 + LaunchedEffect(Unit) { + delay(300) + focusRequesters[0].requestFocus() + keyboardController?.show() + } + + // 화면 종료 시 키보드 숨기기 + DisposableEffect(Unit) { + onDispose { + keyboardController?.hide() + } + } + + val fullCode = verificationCode.joinToString("") + val isComplete = fullCode.length == 6 + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier.size(18.dp) + ) + } + + Text( + text = stringResource(Res.string.email_verification_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 68.dp) + ) + + // 6자리 인증번호 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + repeat(6) { index -> + NumberInputBox( + value = verificationCode[index], + onValueChange = { input -> + if (input.length <= 1 && input.all { it.isDigit() }) { + val newCode = verificationCode.copyOf() + val wasEmpty = verificationCode[index].isEmpty() + newCode[index] = input + verificationCode = newCode + + if (input.isNotEmpty() && index < 5) { + currentFocusIndex = index + 1 + focusRequesters[index + 1].requestFocus() + } else if (input.isEmpty() && !wasEmpty && index > 0) { + currentFocusIndex = index - 1 + focusRequesters[index - 1].requestFocus() + } + } + }, + onBackspace = { + // 빈 박스에서 백스페이스 시 이전 박스로 이동 + if (index > 0) { + val prevIndex = index - 1 + currentFocusIndex = prevIndex + focusRequesters[prevIndex].requestFocus() + } + }, + onFocusChanged = { isFocused -> + if (isFocused) { + currentFocusIndex = index + } + }, + isFocused = currentFocusIndex == index, + modifier = Modifier + .weight(1f) + .focusRequester(focusRequesters[index]) + ) + } + } + } + + // 하단 확인 버튼 + CommonLoginButton( + text = stringResource(Res.string.confirm_button), + onClick = { onVerificationComplete(fullCode) }, + enabled = isComplete, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + +@Preview +@Composable +fun VerificationCodeScreenPreview() { + EmailVerificationScreen( + modifier = Modifier, + onNavigateBack = {}, + onVerificationComplete = { code -> + // 인증번호 처리 로직 + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt new file mode 100644 index 0000000..c8d3766 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/FindPasswordScreen.kt @@ -0,0 +1,108 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.CommonLoginInputField +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.email_placeholder +import whosinclient.composeapp.generated.resources.ic_back +import whosinclient.composeapp.generated.resources.password_reset_title +import whosinclient.composeapp.generated.resources.send_email_button + +@Composable +fun FindPasswordScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onPasswordResetComplete: (String) -> Unit = {} +) { + var email by remember { mutableStateOf("") } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier + .size(18.dp) + ) + } + + Text( + text = stringResource(Res.string.password_reset_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 68.dp) + ) + + CommonLoginInputField( + value = email, + onValueChange = { email = it }, + placeholder = stringResource(Res.string.email_placeholder) + ) + } + + CommonLoginButton( + text = stringResource(Res.string.send_email_button), + onClick = { onPasswordResetComplete(email) }, + enabled = email.isNotBlank(), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + +@Preview +@Composable +fun PasswordResetScreenPreview() { + FindPasswordScreen( + modifier = Modifier, + onNavigateBack = {}, + onPasswordResetComplete = { email -> + // 이메일 처리 로직 + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt new file mode 100644 index 0000000..871d8ed --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/LoginScreen.kt @@ -0,0 +1,161 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.CommonLoginInputField +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.email_label +import whosinclient.composeapp.generated.resources.email_placeholder +import whosinclient.composeapp.generated.resources.find_password_button +import whosinclient.composeapp.generated.resources.img_logo_orange +import whosinclient.composeapp.generated.resources.login_button +import whosinclient.composeapp.generated.resources.login_title +import whosinclient.composeapp.generated.resources.password_label +import whosinclient.composeapp.generated.resources.password_placeholder +import whosinclient.composeapp.generated.resources.signup_button + +@Composable +fun LoginScreen( + modifier: Modifier = Modifier, + onNavigateToHome: () -> Unit, + onNavigateToFindPassword: () -> Unit = {}, + onNavigateToSignup: () -> Unit = {} +) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 96.dp, start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(Res.drawable.img_logo_orange), + contentDescription = "logo", + modifier = Modifier.size(width = 160.dp, height = 122.dp) + ) + Text( + text = stringResource(Res.string.login_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + modifier = Modifier.padding(top = 16.dp, bottom = 32.dp) + ) + + + Text( + text = stringResource(Res.string.email_label), + fontWeight = FontWeight.W500, + fontSize = 16.sp, + modifier = Modifier + .align(Alignment.Start) + .padding(bottom = 10.dp) + ) + CommonLoginInputField( + value = email, + onValueChange = { email = it }, + placeholder = stringResource(Res.string.email_placeholder), + modifier = Modifier.padding(bottom = 10.dp) + ) + + Text( + text = stringResource(Res.string.password_label), + fontWeight = FontWeight.W500, + fontSize = 16.sp, + modifier = Modifier + .align(Alignment.Start) + .padding(bottom = 10.dp) + ) + CommonLoginInputField( + value = password, + onValueChange = { password = it }, + placeholder = stringResource(Res.string.password_placeholder), + isPassword = true, + modifier = Modifier.padding(bottom = 16.dp) + ) + + CommonLoginButton( + text = stringResource(Res.string.login_button), + onClick = onNavigateToHome, + modifier = Modifier.padding(bottom = 12.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = onNavigateToSignup, + modifier = Modifier.padding(horizontal = 4.dp), + colors = ButtonDefaults.textButtonColors( + contentColor = Color.Black + ) + ) { + Text( + text = stringResource(Res.string.signup_button), + fontWeight = FontWeight.W500, + fontSize = 16.sp + ) + } + + TextButton( + onClick = onNavigateToFindPassword, + modifier = Modifier.padding(horizontal = 4.dp), + colors = ButtonDefaults.textButtonColors( + contentColor = Color.Black + ) + ) { + Text( + text = stringResource(Res.string.find_password_button), + fontWeight = FontWeight.W500, + fontSize = 16.sp + ) + } + } + } + } +} + +@Preview +@Composable +fun LoginScreenPreview() { + LoginScreen( + modifier = Modifier, + onNavigateToHome = {}, + onNavigateToFindPassword = {}, + onNavigateToSignup = {} + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt new file mode 100644 index 0000000..e023f9c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/NicknameInputScreen.kt @@ -0,0 +1,120 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.CommonLoginInputField +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.ic_back +import whosinclient.composeapp.generated.resources.next_button +import whosinclient.composeapp.generated.resources.nickname_input_placeholder +import whosinclient.composeapp.generated.resources.nickname_input_title +import whosinclient.composeapp.generated.resources.nickname_welcome_title + +@Composable +fun NicknameInputScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onNavigateToClubCode: (String) -> Unit = {} +) { + var nickname by remember { mutableStateOf("") } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier.size(18.dp) + ) + } + + Text( + text = stringResource(Res.string.nickname_welcome_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Text( + text = stringResource(Res.string.nickname_input_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 32.dp) + ) + + CommonLoginInputField( + value = nickname, + onValueChange = { newValue -> + nickname = newValue + }, + placeholder = stringResource(Res.string.nickname_input_placeholder), + maxLength = 8 + ) + } + + // 하단 다음 버튼 + CommonLoginButton( + text = stringResource(Res.string.next_button), + onClick = { onNavigateToClubCode(nickname) }, + enabled = nickname.isNotBlank(), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + +@Preview +@Composable +fun NicknameInputScreenPreview() { + NicknameInputScreen( + modifier = Modifier, + onNavigateBack = {}, + onNavigateToClubCode = { nickname -> + // 닉네임 처리 로직 + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt new file mode 100644 index 0000000..4aa1c5a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/PasswordInputScreen.kt @@ -0,0 +1,145 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.CommonLoginInputField +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.ic_back +import whosinclient.composeapp.generated.resources.next_button +import whosinclient.composeapp.generated.resources.password_confirm_label +import whosinclient.composeapp.generated.resources.password_confirm_placeholder +import whosinclient.composeapp.generated.resources.password_input_label +import whosinclient.composeapp.generated.resources.password_input_placeholder +import whosinclient.composeapp.generated.resources.password_input_title + +@Composable +fun PasswordInputScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onPasswordComplete: (String, String) -> Unit +) { + var password by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + + val isComplete = password.isNotBlank() && confirmPassword.isNotBlank() + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier.size(18.dp) + ) + } + + Text( + text = stringResource(Res.string.password_input_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 68.dp) + ) + + // 비밀번호 입력 라벨 + Text( + text = stringResource(Res.string.password_input_label), + fontWeight = FontWeight.W500, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier + .padding(bottom = 10.dp) + ) + + // 비밀번호 입력 필드 + CommonLoginInputField( + value = password, + onValueChange = { password = it }, + placeholder = stringResource(Res.string.password_input_placeholder), + isPassword = true, + modifier = Modifier.padding(bottom = 10.dp) + ) + + // 비밀번호 확인 라벨 + Text( + text = stringResource(Res.string.password_confirm_label), + fontWeight = FontWeight.W500, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier + .padding(bottom = 10.dp) + ) + + // 비밀번호 확인 입력 필드 + CommonLoginInputField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + placeholder = stringResource(Res.string.password_confirm_placeholder), + isPassword = true + ) + } + + // 하단 다음 버튼 + CommonLoginButton( + text = stringResource(Res.string.next_button), + onClick = { onPasswordComplete(password, confirmPassword) }, + enabled = isComplete, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + +@Preview +@Composable +fun PasswordInputScreenPreview() { + PasswordInputScreen( + modifier = Modifier, + onNavigateBack = {}, + onPasswordComplete = { password, confirmPassword -> + // 비밀번호 처리 로직 + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt new file mode 100644 index 0000000..e0bdc43 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SignupEmailInputScreen.kt @@ -0,0 +1,109 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.whosin.client.presentation.auth.login.component.CommonLoginButton +import org.whosin.client.presentation.auth.login.component.CommonLoginInputField +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.back_button +import whosinclient.composeapp.generated.resources.email_placeholder +import whosinclient.composeapp.generated.resources.ic_back +import whosinclient.composeapp.generated.resources.next_button +import whosinclient.composeapp.generated.resources.signup_title + +@Composable +fun SignupScreen( + modifier: Modifier = Modifier, + onNavigateBack: () -> Unit = {}, + onNavigateToEmailVerification: (String) -> Unit = {} +) { + var email by remember { mutableStateOf("") } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 30.dp), + horizontalAlignment = Alignment.Start + ) { + IconButton( + onClick = onNavigateBack, + modifier = Modifier + .padding(bottom = 32.dp) + .size(24.dp) + ) { + Icon( + painter = painterResource(Res.drawable.ic_back), + contentDescription = stringResource(Res.string.back_button), + tint = Color.Black, + modifier = Modifier + .size(18.dp) + ) + } + + Text( + text = stringResource(Res.string.signup_title), + fontWeight = FontWeight.W600, + fontSize = 24.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 68.dp) + ) + + CommonLoginInputField( + value = email, + onValueChange = { email = it }, + placeholder = stringResource(Res.string.email_placeholder) + ) + } + + CommonLoginButton( + text = stringResource(Res.string.next_button), + onClick = { onNavigateToEmailVerification(email) }, + enabled = email.isNotBlank(), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 52.dp) + ) + } +} + + +@Preview +@Composable +fun EmailInputScreenPreview() { + SignupScreen( + modifier = Modifier, + onNavigateBack = {}, + onNavigateToEmailVerification = { email -> + // 이메일 처리 로직 + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt new file mode 100644 index 0000000..616f400 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/SplashScreen.kt @@ -0,0 +1,49 @@ +package org.whosin.client.presentation.auth.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import whosinclient.composeapp.generated.resources.Res +import whosinclient.composeapp.generated.resources.img_logo_white + +@Composable +fun SplashScreen( + modifier: Modifier = Modifier, + onNavigateToLogin: () -> Unit = {} +) { + + LaunchedEffect(Unit) { + delay(2000) + onNavigateToLogin() + } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color(0xFFF89531)), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(Res.drawable.img_logo_white), + contentDescription = "logo", + modifier = Modifier.size(width = 160.dp, height = 122.dp) + ) + } +} + +@Preview +@Composable +fun SplashScreenPreview() { + SplashScreen() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt new file mode 100644 index 0000000..9830fb0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginButton.kt @@ -0,0 +1,69 @@ +package org.whosin.client.presentation.auth.login.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.ui.tooling.preview.Preview +import ui.theme.WhosInTheme + +@Composable +fun CommonLoginButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + backgroundColor: Color = Color(0xFFF89531), // 오렌지색 + textColor: Color = Color.White +) { + Button( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(54.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = textColor, + disabledContainerColor = Color(0xFFD2D2D2), + disabledContentColor = Color.White + ), + enabled = enabled + ) { + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.W600 + ) + } +} + +@Preview +@Composable +fun CommonLoginButtonPreview() { + WhosInTheme { + CommonLoginButton( + text = "로그인", + onClick = {} + ) + } +} + + +@Preview +@Composable +fun CommonLoginButtonDisabledPreview() { + CommonLoginButton( + text = "비활성화된 버튼", + onClick = {}, + enabled = false + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt new file mode 100644 index 0000000..4c9d1c8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/CommonLoginInputField.kt @@ -0,0 +1,108 @@ +package org.whosin.client.presentation.auth.login.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun CommonLoginInputField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + isPassword: Boolean = false, + maxLength: Int? = null +) { + var passwordVisible by remember { mutableStateOf(false) } + val isMaxLengthReached = maxLength != null && value.length >= maxLength + OutlinedTextField( + value = value, + onValueChange = { if (maxLength == null || it.length <= maxLength) onValueChange(it) }, + placeholder = { + Text( + text = placeholder, + color = Color(0xFFB2B2B2), + fontSize = 16.sp, + fontWeight = FontWeight.W500 + ) + }, + textStyle = TextStyle( + color = Color.Black, + fontSize = 16.sp, + fontWeight = FontWeight.W500 + ), + modifier = modifier + .height(54.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = if (isMaxLengthReached) Color(0xFFE5E5E5) else Color(0xFFF89531), + unfocusedBorderColor = Color(0xFFE5E5E5), + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + cursorColor = Color(0xFFB2B2B2) + ), + visualTransformation = if (isPassword && !passwordVisible) PasswordVisualTransformation() else VisualTransformation.None, + trailingIcon = if (isPassword && value.isNotEmpty()) { + { + IconButton( + onClick = { passwordVisible = !passwordVisible } + ) { + Icon( + imageVector = if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = if (passwordVisible) "비밀번호 숨기기" else "비밀번호 보기", + tint = Color(0xFFB2B2B2), + modifier = Modifier.size(20.dp) + ) + } + } + } else null, + singleLine = true + ) +} + +@Preview() +@Composable +fun CommonLoginInputFieldPreview() { + var text by remember { mutableStateOf("") } + CommonLoginInputField( + value = text, + onValueChange = { text = it }, + placeholder = "이메일을 입력해주세요" + ) +} + +@Preview +@Composable +fun CommonLoginInputFieldPasswordPreview() { + var password by remember { mutableStateOf("password123") } + CommonLoginInputField( + value = password, + onValueChange = { password = it }, + placeholder = "비밀번호를 입력해주세요", + isPassword = true + ) +} diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt new file mode 100644 index 0000000..105a791 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/component/NumberInputBox.kt @@ -0,0 +1,111 @@ +package org.whosin.client.presentation.auth.login.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun NumberInputBox( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + onBackspace: (() -> Unit)? = null, + onFocusChanged: ((Boolean) -> Unit)? = null, + containerColor: Color = Color.White, + borderColor: Color = Color(0xFFE5E5E5), + focusedBorderColor: Color = Color(0xFFF89531), + isFocused: Boolean = false +) { + + // 커서 위치 제어용 + val textFieldValue = remember(value) { + val displayText = value.ifEmpty { "\u200B" } + TextFieldValue( + text = displayText, + selection = TextRange(displayText.length) + ) + } + + val textStyle = TextStyle( + color = Color.Black, + fontSize = 24.sp, + fontWeight = FontWeight.W600, + textAlign = TextAlign.Center + ) + + val currentBorderColor = if (isFocused) focusedBorderColor else borderColor + + Box( + modifier = modifier + .size(width = 50.dp, height = 54.dp) + .background(containerColor, RoundedCornerShape(8.dp)) + .border(1.dp, currentBorderColor, RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center + ) { + BasicTextField( + value = textFieldValue, + onValueChange = { newValue -> + val cleaned = newValue.text.replace("\u200B", "") + val filtered = cleaned.filter { it.isDigit() }.take(1) + + if (newValue.text.isEmpty() && value.isEmpty()) { + onBackspace?.invoke() + return@BasicTextField + } + + onValueChange(filtered) + }, + textStyle = textStyle.copy( + color = if (value.isEmpty()) Color.Transparent else Color.Black + ), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + cursorBrush = SolidColor(Color(0xFFB2B2B2)), + modifier = Modifier.onFocusChanged { focusState -> + onFocusChanged?.invoke(focusState.isFocused) + }, + decorationBox = { innerTextField -> + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + innerTextField() + } + } + ) + } +} + +@Preview +@Composable +fun NumberInputBoxPreview() { + var digit by remember { mutableStateOf("") } + NumberInputBox( + value = digit, + onValueChange = { digit = it } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginViewModel.kt b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginViewModel.kt rename to composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt index afbad3b..7517f45 100644 --- a/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/LoginViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/whosin/client/presentation/auth/login/viewmodel/LoginViewModel.kt @@ -1,4 +1,4 @@ -package org.whosin.client.presentation.auth +package org.whosin.client.presentation.auth.login.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope