From 24efd661f351fdf53017650c899a98e1f1fe2233 Mon Sep 17 00:00:00 2001 From: Chea-yunzi Date: Sat, 17 Jan 2026 19:04:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8E=A8=20=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + app/build.gradle.kts | 1 + .../com/example/linku_android/MainActivity.kt | 7 +- .../java/com/example/linku_android/MainApp.kt | 72 +- .../java/com/example/linku_android/Splash.kt | 40 +- .../data/api/dto/server/UserInfoDTO.kt | 7 +- .../repository/UserRepositoryImpl.kt | 21 +- .../com/example/design/theme/color/Basic.kt | 2 +- .../design/theme/color/ThemeColorScheme.kt | 5 + .../com/example/design/util/SystemBars.kt | 42 +- .../design/util/rememberFigmaDimens.kt | 40 ++ .../com/example/home/screen/HomeScreen.kt | 8 + feature/login/build.gradle.kts | 1 + .../main/java/com/example/login/LoginApp.kt | 6 +- .../example/login/{auth => }/LoginScreen.kt | 61 +- .../main/java/com/example/login/Typography.kt | 17 - .../example/login/auth/EmailLoginScreen.kt | 292 --------- .../example/login/auth/ResetPasswordScreen.kt | 614 ------------------ .../login/auth/SignUpNicknameScreen.kt | 323 --------- .../login/auth/SignUpPasswordScreen.kt | 403 ------------ .../login/auth/TermsAgreementContent.kt | 185 ------ .../{auth => ui/alert}/PasswordResetAlert.kt | 42 +- .../animation}/AnimatedLoginScreen.kt | 58 +- .../ui/bottom_sheet/NoAnimBottomSheet.kt | 24 +- .../ui/bottom_sheet/TermsAgreementSheet.kt | 5 +- .../login/ui/content/TermsAgreementContent.kt | 69 +- .../example/login/ui/item/AgreementItem.kt | 41 +- .../example/login/ui/item/BackIconButton.kt | 2 +- .../login/ui/item/BottomGradientButton.kt | 45 +- .../example/login/ui/item/CheckIndicator.kt | 62 ++ .../com/example/login/ui/item/CircleItem.kt | 11 +- .../login/ui/item/GradientButtonCore.kt | 51 +- .../example/login/ui/item/LoginTextField.kt | 125 ++-- .../com/example/login/ui/item/OptionButton.kt | 68 +- .../login/ui/item/PasswordLoginTextField.kt | 167 +---- .../example/login/ui/item/PasswordRuleItem.kt | 81 +++ .../login/ui/item/ResetPasswordTopHeader.kt | 24 +- .../login/ui/item/SocialLoginButton.kt | 47 +- .../example/login/ui/item/StepIndicator.kt | 20 +- .../login/ui/screen/EmailLoginScreen.kt | 257 ++++++++ .../screen}/EmailVerificationScreen.kt | 100 +-- .../screen}/InterestContentScreen.kt | 40 +- .../screen}/InterestPurposeScreen.kt | 41 +- .../login/ui/screen/ResetPasswordScreen.kt | 171 +++++ .../{auth => ui/screen}/SignUpGenderScreen.kt | 103 ++- .../{auth => ui/screen}/SignUpJobScreen.kt | 105 ++- .../login/ui/screen/SignUpNicknameScreen.kt | 211 ++++++ .../login/ui/screen/SignUpPasswordScreen.kt | 283 ++++++++ .../login/{auth => ui/screen}/SignUpScreen.kt | 59 +- .../{auth => ui/screen}/WelcomeScreen.kt | 80 ++- .../terms}/MarketingTermsScreen.kt | 29 +- .../{auth => ui/terms}/PrivacyTermsScreen.kt | 28 +- .../{auth => ui/terms}/ServiceTermsScreen.kt | 27 +- .../{auth => ui/terms}/TermsDetailScreen.kt | 6 +- .../{auth => viewmodel}/EmailAuthViewModel.kt | 32 +- .../{auth => viewmodel}/LoginViewModel.kt | 22 +- .../ResetPasswordViewModel.kt | 4 +- .../{auth => viewmodel}/SignUpViewModel.kt | 11 +- .../src/main/res/drawable/ic_login_check.png | Bin 0 -> 410 bytes .../main/res/drawable/ic_login_checkbox.png | Bin 0 -> 1028 bytes .../main/res/drawable/img_recent_login.png | Bin 0 -> 4827 bytes gradle/libs.versions.toml | 2 + 62 files changed, 1964 insertions(+), 2738 deletions(-) create mode 100644 design/src/main/java/com/example/design/util/rememberFigmaDimens.kt rename feature/login/src/main/java/com/example/login/{auth => }/LoginScreen.kt (79%) delete mode 100644 feature/login/src/main/java/com/example/login/Typography.kt delete mode 100644 feature/login/src/main/java/com/example/login/auth/EmailLoginScreen.kt delete mode 100644 feature/login/src/main/java/com/example/login/auth/ResetPasswordScreen.kt delete mode 100644 feature/login/src/main/java/com/example/login/auth/SignUpNicknameScreen.kt delete mode 100644 feature/login/src/main/java/com/example/login/auth/SignUpPasswordScreen.kt delete mode 100644 feature/login/src/main/java/com/example/login/auth/TermsAgreementContent.kt rename feature/login/src/main/java/com/example/login/{auth => ui/alert}/PasswordResetAlert.kt (78%) rename feature/login/src/main/java/com/example/login/{auth => ui/animation}/AnimatedLoginScreen.kt (61%) create mode 100644 feature/login/src/main/java/com/example/login/ui/item/CheckIndicator.kt create mode 100644 feature/login/src/main/java/com/example/login/ui/item/PasswordRuleItem.kt create mode 100644 feature/login/src/main/java/com/example/login/ui/screen/EmailLoginScreen.kt rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/EmailVerificationScreen.kt (83%) rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/InterestContentScreen.kt (89%) rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/InterestPurposeScreen.kt (90%) create mode 100644 feature/login/src/main/java/com/example/login/ui/screen/ResetPasswordScreen.kt rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/SignUpGenderScreen.kt (60%) rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/SignUpJobScreen.kt (62%) create mode 100644 feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt create mode 100644 feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/SignUpScreen.kt (73%) rename feature/login/src/main/java/com/example/login/{auth => ui/screen}/WelcomeScreen.kt (67%) rename feature/login/src/main/java/com/example/login/{auth => ui/terms}/MarketingTermsScreen.kt (92%) rename feature/login/src/main/java/com/example/login/{auth => ui/terms}/PrivacyTermsScreen.kt (92%) rename feature/login/src/main/java/com/example/login/{auth => ui/terms}/ServiceTermsScreen.kt (92%) rename feature/login/src/main/java/com/example/login/{auth => ui/terms}/TermsDetailScreen.kt (93%) rename feature/login/src/main/java/com/example/login/{auth => viewmodel}/EmailAuthViewModel.kt (92%) rename feature/login/src/main/java/com/example/login/{auth => viewmodel}/LoginViewModel.kt (90%) rename feature/login/src/main/java/com/example/login/{auth => viewmodel}/ResetPasswordViewModel.kt (92%) rename feature/login/src/main/java/com/example/login/{auth => viewmodel}/SignUpViewModel.kt (97%) create mode 100644 feature/login/src/main/res/drawable/ic_login_check.png create mode 100644 feature/login/src/main/res/drawable/ic_login_checkbox.png create mode 100644 feature/login/src/main/res/drawable/img_recent_login.png diff --git a/.gitignore b/.gitignore index a962a223..b9ff628e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.iml +*.jks +*.keystore .gradle /local.properties .idea/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4e87e8b8..d8765d0f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,6 +12,7 @@ android { namespace = "com.example.linku_android" compileSdk = 35 + defaultConfig { applicationId = "com.example.linku_android" minSdk = 26 diff --git a/app/src/main/java/com/example/linku_android/MainActivity.kt b/app/src/main/java/com/example/linku_android/MainActivity.kt index 42b570d1..9b246411 100644 --- a/app/src/main/java/com/example/linku_android/MainActivity.kt +++ b/app/src/main/java/com/example/linku_android/MainActivity.kt @@ -4,12 +4,8 @@ import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.material3.Text import androidx.core.view.WindowCompat import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.rememberNavController -import com.example.login.auth.AnimatedLoginScreen import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -20,6 +16,7 @@ class MainActivity : ComponentActivity() { intent?.data?.let { Log.d("DEEPLINK", "onCreate uri = $it") } + WindowCompat.setDecorFitsSystemWindows(window, false) //enableEdgeToEdge() setContent { @@ -29,5 +26,7 @@ class MainActivity : ComponentActivity() { } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/linku_android/MainApp.kt b/app/src/main/java/com/example/linku_android/MainApp.kt index 317c61fe..ea90c228 100644 --- a/app/src/main/java/com/example/linku_android/MainApp.kt +++ b/app/src/main/java/com/example/linku_android/MainApp.kt @@ -54,21 +54,21 @@ import androidx.navigation.compose.currentBackStackEntryAsState import com.example.home.HomeApp import com.example.curation.ui.CurationDetailScreen import com.example.curation.ui.CurationScreen -import com.example.login.auth.AnimatedLoginScreen -import com.example.login.auth.EmailVerificationScreen -import com.example.login.auth.ServiceTermsScreen -import com.example.login.auth.PrivacyTermsScreenFixed -import com.example.login.auth.MarketingTermsScreenComposable -import com.example.login.auth.SignUpPasswordScreen -import com.example.login.auth.EmailLoginScreen -import com.example.login.auth.InterestContentScreen -import com.example.login.auth.InterestPurposeScreen -import com.example.login.auth.SignUpGenderScreen -import com.example.login.auth.SignUpNicknameScreen -import com.example.login.auth.SignUpJobScreen -import com.example.login.auth.WelcomeScreen -import com.example.login.auth.ResetPasswordScreen -import com.example.login.auth.SignUpViewModel +import com.example.login.ui.animation.AnimatedLoginScreen +import com.example.login.ui.screen.EmailVerificationScreen +import com.example.login.ui.terms.ServiceTermsScreen +import com.example.login.ui.terms.PrivacyTermsScreenFixed +import com.example.login.ui.terms.MarketingTermsScreenComposable +import com.example.login.ui.screen.SignUpPasswordScreen +import com.example.login.ui.screen.EmailLoginScreen +import com.example.login.ui.screen.InterestContentScreen +import com.example.login.ui.screen.InterestPurposeScreen +import com.example.login.ui.screen.SignUpGenderScreen +import com.example.login.ui.screen.SignUpNicknameScreen +import com.example.login.ui.screen.SignUpJobScreen +import com.example.login.ui.screen.WelcomeScreen +import com.example.login.ui.screen.ResetPasswordScreen +import com.example.login.viewmodel.SignUpViewModel import java.io.File import java.io.FileOutputStream @@ -82,7 +82,7 @@ import com.example.file.ui.theme.DefaultFont import com.example.file.ui.theme.Gray600 import com.example.file.viewmodel.folder.state.FolderStateViewModel import com.example.linku_android.deeplink.DeepLinkHandlerViewModel -import com.example.login.auth.LoginViewModel +import com.example.login.viewmodel.LoginViewModel import dagger.hilt.android.EntryPointAccessors import androidx.core.net.toUri @@ -268,9 +268,6 @@ fun MainApp( .getStateFlow("show_terms_sheet", false) .collectAsStateWithLifecycle() - BackHandler(enabled = showTermsSheet) { - parentEntry.savedStateHandle["show_terms_sheet"] = false - } // 이메일 인증에서 백버튼으로 갔을 때, 약관 페이지 나오는게 맞는지. @@ -296,8 +293,13 @@ fun MainApp( } + val skipAnimation = + parentEntry.savedStateHandle + .get("skip_login_animation") == true + AnimatedLoginScreen( navigator = navigator, + skipAnimation = skipAnimation, // 백버튼시 애니메이션 스탑 플래그 전달 onSignUpClick = { parentEntry.savedStateHandle["show_terms_sheet"] = true } @@ -310,6 +312,12 @@ fun MainApp( val parentEntry = remember(entry) { navigator.getBackStackEntry("auth_graph") } val vm: SignUpViewModel = hiltViewModel(parentEntry) + //시스템 백버튼 처리 + BackHandler { + parentEntry.savedStateHandle["show_terms_sheet"] = true + navigator.popBackStack() + } + ServiceTermsScreen( onBackClicked = { parentEntry.savedStateHandle["show_terms_sheet"] = true @@ -368,6 +376,15 @@ fun MainApp( val vm: SignUpViewModel = hiltViewModel(parentEntry) + //백버튼으로 온 경우 애니메이션 적용X + BackHandler { + // 로그인 화면(AnimatedLoginScreen)에 애니메이션 스킵 플래그 전달함. + parentEntry.savedStateHandle["skip_login_animation"] = true + parentEntry.savedStateHandle["from_email_verification"] = true + + navigator.popBackStack() + } + EmailVerificationScreen( navigator = navigator, parentEntry = parentEntry, // ⬅ 추가 @@ -441,6 +458,11 @@ fun MainApp( .getStateFlow("show_terms_sheet", false) .collectAsStateWithLifecycle() + //약관 바텀시트 떠 있을 때 백버튼 = 시트 닫기 + BackHandler(enabled = showTermsSheet) { + parentEntry.savedStateHandle["show_terms_sheet"] = false + } + LaunchedEffect(Unit) { showNavBar = false } // 로그인 상태 관찰 @@ -821,12 +843,20 @@ fun MainApp( // ❺ 실제 로그인 UI(AnimatedLoginScreen 등) 렌더링 // AnimatedLoginScreen(navigator = navigator) - AnimatedLoginScreen(navigator = navigator, onSignUpClick = {}) + val skipAnimation = + backStackEntry.savedStateHandle + .get("skip_login_animation") == true + + AnimatedLoginScreen( + navigator = navigator, + skipAnimation = skipAnimation, + onSignUpClick = {} + ) } - // TODO: 로그인 되어 있지 않은 상황 처리 + // TODO: 로그인 되어 있지 않은 상황 처리 ?이게 뭐람 // 링크 공유 앱링크 composable( route = "open?action={action}&folderId={folderId}", diff --git a/app/src/main/java/com/example/linku_android/Splash.kt b/app/src/main/java/com/example/linku_android/Splash.kt index 68124418..a9263cde 100644 --- a/app/src/main/java/com/example/linku_android/Splash.kt +++ b/app/src/main/java/com/example/linku_android/Splash.kt @@ -29,13 +29,7 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import com.example.design.util.PixelScaler -import android.app.Activity -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsControllerCompat -import android.view.WindowInsets +import com.example.design.util.DesignSystemBars @@ -50,31 +44,13 @@ interface SplashDeps { @Composable fun Splash(onResult: (Boolean) -> Unit) { - val view = LocalView.current - val isPreview = LocalInspectionMode.current - - if (!isPreview) { - val activity = view.context as Activity - val window = activity.window - - SideEffect { - // edge-to-edge - WindowCompat.setDecorFitsSystemWindows(window, false) - - // 상태바 + 네비게이션바 완전 숨김 - WindowInsetsControllerCompat(window, view).apply { - hide( - WindowInsets.Type.statusBars() or - WindowInsets.Type.navigationBars() - ) - - // 스와이프로 잠깐 나타났다가 다시 숨김 - systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } - } - + //바텀바 숨김 + DesignSystemBars( + statusBarColor = Color.Transparent, + navigationBarColor = Color.Transparent, + darkIcons = false, + immersive = true + ) val rotationAnim = remember { Animatable(0f) } var isGlowPhase by remember { mutableStateOf(false) } diff --git a/data/src/main/java/com/example/data/api/dto/server/UserInfoDTO.kt b/data/src/main/java/com/example/data/api/dto/server/UserInfoDTO.kt index 622174b4..0ed53fad 100644 --- a/data/src/main/java/com/example/data/api/dto/server/UserInfoDTO.kt +++ b/data/src/main/java/com/example/data/api/dto/server/UserInfoDTO.kt @@ -4,12 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass data class UserInfoDTO( -// @Json(name = "nickname") -// val nickname: String, - - // TODO: 통합 - @Json(name = "nickname") - val nickname: String? = null, + // Done 통합 : 01.13 완료 했습니다. (username 제거) @Json(name = "nickName") val nickName: String? = null, diff --git a/data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt b/data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt index 8d94ee7e..6d9de1cb 100644 --- a/data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt @@ -151,25 +151,19 @@ class UserRepositoryImpl @Inject constructor( // 마이페이지 조회 override suspend fun getUserInfo(userId: Long): UserInfo { - // val response = userApi.withAuth(authPreference) { getUserInfo(userId) } val response = userApi.getUserInfo(userId) - val dto: UserInfoDTO = response.result + val dto = response.result ?: throw IllegalStateException("마이페이지 조회 실패: ${response.message}") - // DTO -> 도메인 매핑을 여기서 바로 처리 - val nick = dto.nickname ?: dto.nickName ?: "" // ← Fallback 추가 - - // 서버에서 받은 enum 코드 → 화면 한글 라벨로 변환 + // 서버 enum → 한글 val displayPurposes = dto.purposes.map { reversePurposeMap[it] ?: it } val displayInterests = dto.interests.map { reverseInterestMap[it] ?: it } - // DTO -> 도메인 매핑을 여기서 바로 처리 return UserInfo( -// nickname = dto.nickname, - nickname = nick, + nickname = dto.nickName.orEmpty(), email = dto.email, - gender = dto.gender.value, // "MALE" | "FEMALE" + gender = dto.gender.value, jobId = dto.job.id, jobName = dto.job.name, myLinku = dto.myLinku, @@ -224,12 +218,11 @@ class UserRepositoryImpl @Inject constructor( // 닉네임 전용 메서드로 분리 override suspend fun getNickname(userId: Long): String? { return try { - val res = userApi.getUserInfo(userId) // BaseResponse - // 서버 DTO 필드명 대응 (nickname 혹은 nickName) - val nick = res.result?.nickname ?: res.result?.nickName + val res = userApi.getUserInfo(userId) + val nick = res.result?.nickName Log.d("UserRepository", "닉네임=$nick") nick?.takeIf { it.isNotBlank() } - } catch (e: retrofit2.HttpException) { + } catch (e: HttpException) { if (e.code() == 500) null else throw e } catch (e: Exception) { Log.e("UserRepository", "닉네임 가져오기 실패", e) diff --git a/design/src/main/java/com/example/design/theme/color/Basic.kt b/design/src/main/java/com/example/design/theme/color/Basic.kt index c40973cb..df4ce49c 100644 --- a/design/src/main/java/com/example/design/theme/color/Basic.kt +++ b/design/src/main/java/com/example/design/theme/color/Basic.kt @@ -8,7 +8,7 @@ data object Basic: ThemeColorScheme( maincolor= Brush.horizontalGradient( listOf( Color(0xFF2C6FFF), - Color(0xFFCB59EB) + Color(0xFFC800FF) //수정함. ) ), blue = ColorMap( diff --git a/design/src/main/java/com/example/design/theme/color/ThemeColorScheme.kt b/design/src/main/java/com/example/design/theme/color/ThemeColorScheme.kt index e0581126..31dbd919 100644 --- a/design/src/main/java/com/example/design/theme/color/ThemeColorScheme.kt +++ b/design/src/main/java/com/example/design/theme/color/ThemeColorScheme.kt @@ -30,6 +30,11 @@ sealed class ThemeColorScheme( 800 to Color(0xFF43454B), ), + // 비활성화용 그라데이션 브러시 추가 -로그인, 회원가입용 + val inactiveColor: Brush = Brush.horizontalGradient( + listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)) + ), + val black: Color = Color(0xFF000208), val white: Color = Color(0xFFFFFFFF), val positive: Color = Color(0xFF35DF79), diff --git a/design/src/main/java/com/example/design/util/SystemBars.kt b/design/src/main/java/com/example/design/util/SystemBars.kt index d4adfa5e..62803190 100644 --- a/design/src/main/java/com/example/design/util/SystemBars.kt +++ b/design/src/main/java/com/example/design/util/SystemBars.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat /** @@ -19,29 +20,44 @@ import androidx.core.view.WindowInsetsControllerCompat */ @Composable fun DesignSystemBars( - statusBarColor: Color = Color.White, - navigationBarColor: Color = Color.White, - darkIcons: Boolean = true + statusBarColor: Color = Color.White, //상태바 배경 색상 + navigationBarColor: Color = Color.White, //하단 네비게이션 바 배경 색상 + darkIcons: Boolean = true, // true인 경우, 어두운 아이콘(밝은 배경) + immersive: Boolean = false + //스플래쉬, 앱 진입 애니메이션은 디자이너와 상의 끝에 바텀바 안보이도록 함. + // 숨김 기능 추가(immersive: Boolean = false) ) { val view = LocalView.current val isPreview = LocalInspectionMode.current - if (isPreview) return SideEffect { val window = (view.context as Activity).window + val controller = WindowInsetsControllerCompat(window, view) + + WindowCompat.setDecorFitsSystemWindows(window, !immersive) - // edge-to-edge 유지 - WindowCompat.setDecorFitsSystemWindows(window, false) + //immersive 여부에 따라 바텀바 레이아웃 처리 진행함. + if (immersive) { + //스플래쉬, 앱 진입 애니메이션인 경우 + controller.hide( + WindowInsetsCompat.Type.statusBars() or + WindowInsetsCompat.Type.navigationBars() + ) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + //그 외 화면들 원래대로 작동함. + controller.show( + WindowInsetsCompat.Type.statusBars() or + WindowInsetsCompat.Type.navigationBars() + ) + } - // 시스템 바 색상 window.statusBarColor = statusBarColor.toArgb() window.navigationBarColor = navigationBarColor.toArgb() - // 아이콘 색상 - WindowInsetsControllerCompat(window, view).apply { - isAppearanceLightStatusBars = darkIcons - isAppearanceLightNavigationBars = darkIcons - } + controller.isAppearanceLightStatusBars = darkIcons + controller.isAppearanceLightNavigationBars = darkIcons } -} \ No newline at end of file +} diff --git a/design/src/main/java/com/example/design/util/rememberFigmaDimens.kt b/design/src/main/java/com/example/design/util/rememberFigmaDimens.kt new file mode 100644 index 00000000..83c9830c --- /dev/null +++ b/design/src/main/java/com/example/design/util/rememberFigmaDimens.kt @@ -0,0 +1,40 @@ +package com.example.design.util + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/* +* 피그마 레이아웃 기준으로 반응형 ui를 작업할 수 있는 공통 유틸 파일입니다. +* 피그마에서 가로 412 , 세로 917 기준으로 dp를 사용해서 작업했습니다. +* 로그인, 회원가입 임시로 적용중 +* TODO : 유지만 팀장 확인 받기. +* */ + +@Composable +fun rememberFigmaDimens(): Pair<(Float) -> Dp, (Float) -> Dp> { + val config = LocalConfiguration.current //현재 화면 구성을 가져옴.(너비, 높이) + val screenWidth = config.screenWidthDp.dp //피그마에서 설정한 412에 대한 비율을 계싼해서 현재 화면 너미에 맞춰 조정함. + val screenHeight = config.screenHeightDp.dp //피그마에서 설정한 917에 대한 비율을 계산해서 현재 화면 높이에 맞춰 조장힘. + + // 1. 가로 모드인지 확인 + val isLandscape = config.screenWidthDp > config.screenHeightDp + + // 2. 가로 너비의 기준을 잡음 + // (태블릿 가로모드에서 디자인이 너무 퍼지지 않도록 최대 너비를 제한하거나 세로 비율에 맞춤) + val baseWidth = if (isLandscape) { + // 가로모드일 때는 세로 높이의 일정 비율 혹은 고정된 최대 너비(예: 600dp)를 기준으로 잡음 + minOf(screenWidth, 600.dp) + } else { + screenWidth + } + + + // 3. 계산 로직 (가로모드일 때는 중앙 정렬을 위해 w 사용 시 보정 필요할 수 있음) + val w: (Float) -> Dp = { px -> baseWidth * (px / 412f) } + val h: (Float) -> Dp = { px -> screenHeight * (px / 917f) } + + return w to h +} + diff --git a/feature/home/src/main/java/com/example/home/screen/HomeScreen.kt b/feature/home/src/main/java/com/example/home/screen/HomeScreen.kt index eb2d5549..35ef6351 100644 --- a/feature/home/src/main/java/com/example/home/screen/HomeScreen.kt +++ b/feature/home/src/main/java/com/example/home/screen/HomeScreen.kt @@ -58,6 +58,7 @@ import com.example.design.modifier.noRippleClickable import com.example.design.theme.LocalColorTheme import com.example.design.theme.LocalFontTheme import com.example.design.theme.color.Basic +import com.example.design.util.DesignSystemBars import com.example.file.ui.theme.FileTopBarLinkUFont import com.example.file.ui.theme.MainColor import com.example.home.HomeViewModel @@ -130,6 +131,13 @@ fun HomeScreen( jobId: Long, onLinkClick: (linkuId: Long) -> Unit, ) { + //스플래쉬에서 숨긴 시스템 바 다시 뜨도록 + DesignSystemBars( + statusBarColor = Color.White, + navigationBarColor = Color.White, + darkIcons = true, + immersive = false + ) var showRecs by remember { mutableStateOf(showRecommendations) } LaunchedEffect(showRecommendations) { showRecs = showRecommendations } diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index 7195f2ba..28fa77b0 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.tools.core) + implementation(libs.androidx.foundation) androidTestImplementation(libs.androidx.ui.test.android) androidTestImplementation(libs.androidx.compose.testing) testImplementation(libs.junit) diff --git a/feature/login/src/main/java/com/example/login/LoginApp.kt b/feature/login/src/main/java/com/example/login/LoginApp.kt index ab07f259..10d914c9 100644 --- a/feature/login/src/main/java/com/example/login/LoginApp.kt +++ b/feature/login/src/main/java/com/example/login/LoginApp.kt @@ -2,8 +2,10 @@ package com.example.login import androidx.compose.runtime.Composable import androidx.navigation.compose.rememberNavController -import com.example.login.auth.LoginScreen -import com.example.login.auth.LoginViewModel +import com.example.login.viewmodel.LoginViewModel + + +//리펙토링 하면서 필요함을 느껴 생성함. 아직 사용X. @Composable fun LoginApp(viewModel: LoginViewModel) { diff --git a/feature/login/src/main/java/com/example/login/auth/LoginScreen.kt b/feature/login/src/main/java/com/example/login/LoginScreen.kt similarity index 79% rename from feature/login/src/main/java/com/example/login/auth/LoginScreen.kt rename to feature/login/src/main/java/com/example/login/LoginScreen.kt index 7171d81f..d5c53949 100644 --- a/feature/login/src/main/java/com/example/login/auth/LoginScreen.kt +++ b/feature/login/src/main/java/com/example/login/LoginScreen.kt @@ -1,11 +1,8 @@ -package com.example.login.auth +package com.example.login //피그마에서 스플래쉬 다음으로 나오는 로그인 화면 입니다. -import android.app.Activity -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.foundation.Image + import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable @@ -14,10 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.WindowInsets @@ -26,22 +20,14 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.ui.draw.alpha import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.login.R -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import androidx.compose.material3.Text -import androidx.compose.runtime.SideEffect import androidx.compose.ui.text.style.TextAlign -import com.example.login.Paperlogy -import com.example.login.ui.item.SocialLoginButton -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.tooling.preview.Devices -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsControllerCompat -import androidx.core.view.WindowInsetsCompat - +import com.example.design.util.DesignSystemBars +import com.example.login.ui.item.SocialLoginButton +import com.example.design.theme.font.Paperlogy @Composable @@ -51,31 +37,20 @@ fun LoginScreen( contentAlpha: Float = 1f, logoSlot: @Composable () -> Unit = {}, //로고가 들어갈 자리 showLogo: Boolean = true, //로고 숨김(애니메이션 동안) - //emailButtonColor: Color = Color(0x66FFFFFF), - //onSignUpClick: () -> Unit = {} //기존에 이메일로 시작하기 버튼이 반짝이인데 유지할지 말지 물어보기. + ) { + // 2. 디자인 모듈의 폰트 패밀리 변수화 + val paperlogyFamily = Paperlogy.font - val isPreview = LocalInspectionMode.current - val view = LocalView.current - - if (!isPreview) { - val activity = view.context as Activity - val window = activity.window + // 스플래쉬 다음 화면도 역시 바텀바가 보이지 않도록 함. + DesignSystemBars( + statusBarColor = Color.Transparent, + navigationBarColor = Color.Transparent, + darkIcons = false, + immersive = true + ) - SideEffect { - WindowCompat.setDecorFitsSystemWindows(window, false) - - WindowInsetsControllerCompat(window, view).apply { - hide( - WindowInsetsCompat.Type.statusBars() or - WindowInsetsCompat.Type.navigationBars() - ) - systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } - } Box( modifier = Modifier .fillMaxSize() @@ -136,7 +111,7 @@ fun LoginScreen( text = "Link U, Think You", fontSize = 14.sp, lineHeight = 16.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(500), color = Color.White, textAlign = TextAlign.Center @@ -149,7 +124,7 @@ fun LoginScreen( text = "링큐에 오신 것을 \n환영해요", fontSize = 22.sp, lineHeight = 30.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(700), color = Color.White, textAlign = TextAlign.Center @@ -209,7 +184,7 @@ fun LoginScreen( textColor = Color.Black ) - // 이메일 기존 그대로 유지. //TODO 채윤지 : 하진 언니로부터 샌드 그리드에서 변경된 api 발생시 재연동 작업 하기.... + // 이메일 기존 그대로 유지. //TODO 채윤지 : 서원에게 변경된 otp api 받으면 재연동하기 SocialLoginButton( backgroundColor = Color.Transparent, borderColor = Color.White, diff --git a/feature/login/src/main/java/com/example/login/Typography.kt b/feature/login/src/main/java/com/example/login/Typography.kt deleted file mode 100644 index 00b4a686..00000000 --- a/feature/login/src/main/java/com/example/login/Typography.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.login - -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight - -val Paperlogy = FontFamily( - Font(R.font.paperlogy_1thin, FontWeight.Thin), - Font(R.font.paperlogy_2extralight, FontWeight.ExtraLight), - Font(R.font.paperlogy_3light, FontWeight.Light), - Font(R.font.paperlogy_4regular, FontWeight.Normal), - Font(R.font.paperlogy_5medium, FontWeight.Medium), - Font(R.font.paperlogy_6semibold, FontWeight.SemiBold), - Font(R.font.paperlogy_7bold, FontWeight.Bold), - Font(R.font.paperlogy_8extrabold, FontWeight.ExtraBold), - Font(R.font.paperlogy_9black, FontWeight.Black) -) \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/auth/EmailLoginScreen.kt b/feature/login/src/main/java/com/example/login/auth/EmailLoginScreen.kt deleted file mode 100644 index 97cb2735..00000000 --- a/feature/login/src/main/java/com/example/login/auth/EmailLoginScreen.kt +++ /dev/null @@ -1,292 +0,0 @@ -package com.example.login.auth - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.example.login.R -import com.example.login.Paperlogy -import com.example.login.ui.item.BottomGradientButton -import com.example.login.ui.item.LoginTextField -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.BaselineShift -import com.example.login.Paperlogy -import com.example.login.ui.item.GradientButtonCore -import com.example.login.ui.item.PasswordLoginTextField -import com.example.design.modifier.noRippleClickable -import com.example.login.ui.bottom_sheet.TermsAgreementSheet -import android.app.Activity -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsControllerCompat -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.core.view.WindowInsetsCompat -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars -import com.example.design.util.WhiteSystemBars - - -@Composable -fun EmailLoginScreen( - navigator: NavHostController, - loginViewModel: LoginViewModel? = null, - onSignUpClick: () -> Unit -) { - - WhiteSystemBars() //안드로이드 자체 바텀바(흰섹) - val view = LocalView.current - val isPreview = LocalInspectionMode.current - - if (!isPreview) { - val activity = view.context as Activity - val window = activity.window - - SideEffect { - // edge-to-edge 유지 - WindowCompat.setDecorFitsSystemWindows(window, false) - - // 상태바 + 네비게이션바 다시 표시 - WindowInsetsControllerCompat(window, view).apply { - show( - WindowInsetsCompat.Type.statusBars() or - WindowInsetsCompat.Type.navigationBars() - ) - - isAppearanceLightStatusBars = true - isAppearanceLightNavigationBars = true - } - } - } - - - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - - val isEmailValid = - android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() - val isFormValid = email.isNotBlank() && password.isNotBlank() && isEmailValid - - // 🔑 화면 높이 - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp.dp - - // 🔑 키보드 상태 (프리뷰 안전) - val density = LocalDensity.current - val isInPreview = LocalInspectionMode.current - val imeBottom = if (isInPreview) 0 else WindowInsets.ime.getBottom(density) - val isKeyboardOpen = imeBottom > 0 - val buttonOffsetY = if (isKeyboardOpen) 0.dp else (-4).dp - - // 🔑 BottomGradientButton 내부 padding과 동일한 값 계산 - val navBottom = WindowInsets.navigationBars.getBottom(density) - - val buttonInnerPadding = when { - imeBottom > 0 -> 20.dp // 키보드 열림 - navBottom > 0 -> 16.dp // 네비게이션 바 있음 - else -> 24.dp // 풀스크린 - } - - // 🔑 피그마 비율 적용 - val logoRatio = if (isKeyboardOpen) 102f / 917f else 262f / 917f //키보드 활성화 전, 후 - val logoTopPadding = screenHeight * logoRatio - - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - // 로고 위치 (비율 기반, 전체가 같이 이동) - Spacer(modifier = Modifier.height(logoTopPadding)) - - Image( - painter = painterResource(id = R.drawable.ic_logo_color), - contentDescription = "LinkU Logo", - modifier = Modifier - .width(84.62123.dp) - .height(60.00009.dp), - contentScale = ContentScale.Fit - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Link U, Think You", - style = TextStyle( - fontSize = 13.sp, - lineHeight = 15.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight(400), - color = Color(0xFF87898F), - textAlign = TextAlign.Center - ) - ) - - Spacer(modifier = Modifier.height(40.dp)) - - // 🔹 입력 영역 - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - LoginTextField( - value = email, - onValueChange = { email = it }, - hint = "이메일", - textStyle = TextStyle( - fontSize = 14.sp, - lineHeight = 16.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight(500), - color = Color(0xFF000208) - ) - ) - - Spacer(modifier = Modifier.height(10.dp)) - - PasswordLoginTextField( - value = password, - onValueChange = { password = it } - ) - } - - Spacer(modifier = Modifier.height(45.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .offset(y = buttonOffsetY) - ) { - GradientButtonCore( - text = "로그인하기", - enabled = isFormValid, - activeGradient = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ), - inactiveGradient = listOf( - Color(0xFF9BCBFF), - Color(0xFFF4AFFF) - ), - onClick = { - loginViewModel?.login( - email.trim(), - password.trim() - ) - } - ) - } - - Spacer(modifier = Modifier.height(20.dp)) - - - // 🔑 화면 너비 - val screenWidth = LocalConfiguration.current.screenWidthDp.dp - val resetStartPadding = screenWidth * (101f / 412f) - - Box( - modifier = Modifier - .fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(start = resetStartPadding) - ) { - Text( - text = "비밀번호 재설정", - fontSize = 15.sp, - fontFamily = Paperlogy, - color = Color(0xFF87898F), - modifier = Modifier - .alignByBaseline() - .noRippleClickable { - navigator.navigate("resetPassword") - } - ) - - Spacer(modifier = Modifier.width(25.dp)) - - Text( - text = "|", - fontSize = 14.sp, - fontFamily = Paperlogy, - color = Color(0xFF87898F), - style = TextStyle( - baselineShift = BaselineShift(0.15f) - ), - modifier = Modifier.alignByBaseline() - ) -// Image( -// painter = painterResource(id = R.drawable.ic_divider_vertical), -// contentDescription = null, -// modifier = Modifier -// .height(12.dp) -// .alignBy { it.measuredHeight / 2 } //깨짐. -// ) - - Spacer(modifier = Modifier.width(25.dp)) - - Text( - text = "회원가입", - fontSize = 15.sp, - fontFamily = Paperlogy, - color = Color(0xFF87898F), - modifier = Modifier - .alignByBaseline() - .noRippleClickable { - onSignUpClick() - } - ) - } - } - } - } -} - - - - -//@Preview(showBackground = true, name = "Login - Keyboard OFF") -//@Composable -//fun EmailLoginPreview_NoKeyboard() { -// EmailLoginScreen( -// navigator = rememberNavController() -// ) -//} - - - - - diff --git a/feature/login/src/main/java/com/example/login/auth/ResetPasswordScreen.kt b/feature/login/src/main/java/com/example/login/auth/ResetPasswordScreen.kt deleted file mode 100644 index ac4981e1..00000000 --- a/feature/login/src/main/java/com/example/login/auth/ResetPasswordScreen.kt +++ /dev/null @@ -1,614 +0,0 @@ -package com.example.login.auth - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.example.login.Paperlogy -import com.example.login.R -import com.example.login.ui.item.BottomGradientButton -import com.example.login.ui.item.LoginTextField -import com.example.login.ui.item.ResetPasswordTopHeader - -// ======================= -// 실제 Screen (ViewModel 사용) -// ======================= -@Composable -fun ResetPasswordScreen( - navigator: NavHostController, - viewModel: ResetPasswordViewModel? = hiltViewModel() -) { - // 🔑 Preview면 viewModel == null - val ui = viewModel?.ui?.collectAsState()?.value - - var email by remember { mutableStateOf("test@email.com") } - - val isEmailValid = - android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() - - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - Box(modifier = Modifier.fillMaxSize()) { - - ResetPasswordTopHeader( - onBack = { - if (viewModel != null) { - navigator.navigate("email_login") { - popUpTo("resetPassword") { inclusive = true } - } - } - } - ) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.Start - ) { - - // 헤더 밀기 - val headerHeight = - LocalConfiguration.current.screenHeightDp.dp * (59f / 917f) - Spacer(modifier = Modifier.height(headerHeight)) - - Spacer(Modifier.height(38.dp)) - - Image( - painter = painterResource(id = R.drawable.ic_logo_color), - contentDescription = null, - modifier = Modifier - .width(56.41417.dp) - .height(40.00008.dp), - contentScale = ContentScale.Fit - - ) - - Spacer(Modifier.height(18.dp)) - - Text( - text = "비밀번호 재설정", - fontSize = 22.sp, - lineHeight = 30.sp, - fontWeight = FontWeight.Bold, - fontFamily = Paperlogy - ) - - Spacer(Modifier.height(22.dp)) - - Text( - text = "링큐에 가입했던 이메일을 입력해주세요. \n비밀번호를 다시 설정할 수 있는 메일을 보내드릴게요.", - fontSize = 16.sp, - lineHeight = 22.sp, - fontFamily = Paperlogy, - color = Color(0xFF87898F) - ) - - Spacer(Modifier.height(45.dp)) - - LoginTextField( - value = email, - onValueChange = { - email = it - if (ui?.error != null) viewModel?.consumeError() - }, - hint = "이메일 주소를 입력해주세요" - ) - - if (ui?.error != null) { - Spacer(Modifier.height(8.dp)) - Text( - text = ui.error, - color = Color(0xFFFF3B30), - fontSize = 12.sp, - fontFamily = Paperlogy, - modifier = Modifier.padding(start = 8.dp) - ) - } - - Spacer(modifier = Modifier.weight(1f)) - } - - BottomGradientButton( - text = "메일 보내기", - enabled = isEmailValid && (ui?.loading != true), - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), - onClick = { - keyboardController?.hide() - focusManager.clearFocus() - viewModel?.request(email) - }, - modifier = Modifier.align(Alignment.BottomCenter) - ) - } -} - -// ======================= -// Preview (UI 확인 전용) -// ======================= -@Preview(showBackground = true, name = "ResetPassword UI Preview") -@Composable -fun ResetPasswordScreenPreview() { - ResetPasswordScreen( - navigator = rememberNavController(), - viewModel = null // 🔥 이 한 줄이 핵심 - ) -} - - - -//package com.example.login.auth -// -//import androidx.activity.compose.BackHandler -//import androidx.compose.foundation.background -//import androidx.compose.foundation.border -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.shape.RoundedCornerShape -//import androidx.compose.foundation.text.KeyboardOptions -//import androidx.compose.material.icons.Icons -//import androidx.compose.material.icons.filled.MailOutline -//import androidx.compose.material3.* -//import androidx.compose.runtime.* -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.graphics.Brush -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.platform.LocalFocusManager -//import androidx.compose.ui.platform.LocalSoftwareKeyboardController -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.text.input.ImeAction -//import androidx.compose.ui.text.input.KeyboardType -//import androidx.compose.ui.text.input.TextFieldValue -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.navigation.NavHostController -//import androidx.navigation.compose.rememberNavController -//import kotlinx.coroutines.delay -//import kotlinx.coroutines.launch -//import androidx.compose.foundation.layout.imePadding -//import androidx.compose.ui.text.style.TextAlign -//import com.example.login.Paperlogy -//import androidx.compose.foundation.Image -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.res.painterResource -//import androidx.compose.ui.layout.ContentScale -//import com.example.login.R -//import com.example.login.Paperlogy -//import androidx.hilt.navigation.compose.hiltViewModel -//import androidx.compose.foundation.text.KeyboardActions -//import androidx.compose.ui.platform.LocalConfiguration -//import androidx.compose.ui.platform.LocalDensity -//import androidx.compose.ui.unit.Dp -//import com.example.login.ui.item.ResetPasswordTopHeader -// -// -////비밀번호 재설정임! -// -//@Composable -//fun ResetPasswordScreen( -// navigator: NavHostController, -// viewModel: ResetPasswordViewModel = hiltViewModel() -//) { -// var email by remember { mutableStateOf(TextFieldValue("")) } -// val isEmailValid = android.util.Patterns.EMAIL_ADDRESS.matcher(email.text).matches() -// -// val ui = viewModel.ui.collectAsState().value -// var showSuccessDialog by remember { mutableStateOf(false) } -// -// val keyboardController = LocalSoftwareKeyboardController.current -// val focusManager = LocalFocusManager.current -// -// // ✅ 추가: 버튼 전용 바닥 패딩 계산 -// val density = LocalDensity.current -// val imeBottomPx = WindowInsets.ime.getBottom(density) -// val isImeVisible = imeBottomPx > 0 -// val bottomGapWhenIme = 4.dp // 키보드와 버튼 간격 -// val bottomGapDefault = 16.dp // 원래 화면 하단 여백 유지 -// val navBottomDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } -// val extraNavPadding = if (isImeVisible) 0.dp else navBottomDp -// val bottomPadding = (if (isImeVisible) bottomGapWhenIme else bottomGapDefault) + extraNavPadding -// -// BackHandler { -// navigator.navigate("email_login") { -// popUpTo("resetPassword") { inclusive = true } -// } -// } -// -// // 성공시 다이얼로그 노출 -// LaunchedEffect(ui.success) { -// if (ui.success) { -// showSuccessDialog = true -// viewModel.consumeSuccess() -// } -// } -// -// Box(modifier = Modifier.fillMaxSize()) { -// -// // 1. 헤더는 최상단 -// ResetPasswordTopHeader( -// onBack = { -// navigator.navigate("email_login") { -// popUpTo("resetPassword") { inclusive = true } -// } -// } -// ) -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .padding( -// start = 20.dp, -// end = 20.dp, -// bottom = 16.dp -// //bottom = 48.dp + 24.dp // ✅ 하단 버튼(48) + 여유(24) 확보 -// ), -// -// -// //.padding(horizontal = 20.dp, vertical = 52.dp) -// //.imePadding(), -// horizontalAlignment = Alignment.Start -// // horizontalAlignment = Alignment.CenterHorizontally -// ) { -// -// // 헤더 높이만큼 공간 확보 (이게 빠져 있었음) -// Spacer( -// modifier = Modifier.height( -// LocalConfiguration.current.screenHeightDp.dp * (59f / 917f) + -// with(LocalDensity.current) { -// WindowInsets.statusBars.getTop(this).toDp() -// } -// ) -// ) -// -// -// Image( -// painter = painterResource(id = R.drawable.logo_whiteback), -// contentDescription = "Logo", -// modifier = Modifier -// .size(64.dp) -// .align(Alignment.Start), -// contentScale = ContentScale.Fit -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// text = "비밀번호 재설정", -// fontSize = 22.sp, -// fontWeight = FontWeight.Bold, -// fontFamily = Paperlogy, -// color = Color.Black, -// textAlign = TextAlign.Start, -// modifier = Modifier.fillMaxWidth() -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// text = "걱정 마세요! 이메일 주소를 입력해 주시면,\n임시 비밀번호를 보내드릴게요!", -// fontSize = 16.sp, -// fontWeight = FontWeight.Normal, -// fontFamily = Paperlogy, -// color = Color(0xFF87898F), -// textAlign = TextAlign.Start, -// modifier = Modifier.fillMaxWidth() -// ) -// -// Spacer(Modifier.height(32.dp)) -// -// OutlinedTextField( -// value = email, -// onValueChange = { -// email = it -// if (ui.error != null) viewModel.consumeError() -// }, -// placeholder = { -// Text( -// "이메일 주소를 입력해주세요.", -// fontSize = 14.sp, -// fontWeight = FontWeight.Normal, -// fontFamily = Paperlogy, -// color = Color(0xFFB7B9BF) -// ) -// }, -// singleLine = true, -// keyboardOptions = KeyboardOptions( -// keyboardType = KeyboardType.Email, -// imeAction = ImeAction.Done -// ), -// keyboardActions = KeyboardActions( -// onDone = { -// if (isEmailValid && !ui.loading) { -// keyboardController?.hide() -// focusManager.clearFocus() -// viewModel.request(email.text) -// } -// } -// ), -// modifier = Modifier -// .fillMaxWidth() -// .background(Color.White, shape = RoundedCornerShape(16.dp)) -// .border( -// width = 1.dp, -// brush = Brush.horizontalGradient( -// colors = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) -// ), -// shape = RoundedCornerShape(16.dp) -// ), -// colors = TextFieldDefaults.colors( -// focusedIndicatorColor = Color.Transparent, -// unfocusedIndicatorColor = Color.Transparent, -// focusedContainerColor = Color.Transparent, -// unfocusedContainerColor = Color.Transparent -// ), -// shape = RoundedCornerShape(16.dp) -// ) -// -// // 입력 하단 에러 문구 (피그마 #FF3B30 느낌) -// if (ui.error != null) { -// Spacer(Modifier.height(8.dp)) -// Text( -// text = ui.error ?: "", -// color = Color(0xFFFF3B30), -// fontSize = 12.sp, -// fontFamily = Paperlogy, -// modifier = Modifier -// .fillMaxWidth() -// .padding(start = 8.dp) -// ) -// } -// -// Spacer(modifier = Modifier.weight(1f)) -// -// // 제출 버튼 -// // ✅ 하단 버튼: 키보드 보이면 4dp, 아니면 40dp(+내비바) 간격 -// Box( -// modifier = Modifier -// .fillMaxWidth() -// .align(Alignment.CenterHorizontally) -// .padding(start = 20.dp, end = 20.dp, bottom = bottomPadding) -// .height(48.dp) -// .background( -// brush = Brush.horizontalGradient( -// colors = if (isEmailValid) -// listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) -// else -// listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)) -// ), -// shape = RoundedCornerShape(18.dp) -// ) -// .clickable(enabled = isEmailValid && !ui.loading) { -// keyboardController?.hide() -// focusManager.clearFocus() -// viewModel.request(email.text) -// }, -// contentAlignment = Alignment.Center -// ) { -// if (ui.loading) { -// CircularProgressIndicator( -// modifier = Modifier.size(20.dp), -// strokeWidth = 2.dp, -// color = Color.White -// ) -// } else { -// Text( -// text = "임시 비밀번호 받기", -// color = Color.White, -// fontFamily = Paperlogy, -// fontSize = 16.sp, -// fontWeight = FontWeight.Bold -// ) -// } -// } -// -// } -// } -// Spacer(modifier = Modifier.height(32.dp)) -// -// // 성공 팝업 -// if (showSuccessDialog) { -// PasswordResetAlert( -// onDismissRequest = { showSuccessDialog = false }, -// onConfirmClick = { -// showSuccessDialog = false -// navigator.navigate("email_login") { -// popUpTo("resetPassword") { inclusive = true } -// } -// } -// ) -// } -//} -// -//@Preview(showBackground = true, name = "ResetPassword UI Preview") -//@Composable -//fun ResetPasswordScreenContentPreview() { -// ResetPasswordScreenContent( -// email = TextFieldValue("test@email.com"), -// onEmailChange = {}, -// isEmailValid = true, -// loading = false, -// error = null, -// bottomPadding = 16.dp, -// onSubmit = {} -// ) -//} -// -// -// -//@Composable -//fun ResetPasswordScreenContent( -// email: TextFieldValue, -// onEmailChange: (TextFieldValue) -> Unit, -// isEmailValid: Boolean, -// loading: Boolean, -// error: String?, -// onSubmit: () -> Unit, -// bottomPadding: Dp -//) { -// val keyboardController = LocalSoftwareKeyboardController.current -// val focusManager = LocalFocusManager.current -// -// Box(modifier = Modifier.fillMaxSize()) { -// -// // 🔹 헤더 -// ResetPasswordTopHeader(onBack = {}) -// -// Column( -// modifier = Modifier -// .fillMaxSize() -// .padding(start = 20.dp, end = 20.dp), -// horizontalAlignment = Alignment.Start -// ) { -// -// // 🔹 헤더 높이만큼 Spacer -// Spacer( -// modifier = Modifier.height( -// LocalConfiguration.current.screenHeightDp.dp * (59f / 917f) + -// with(LocalDensity.current) { -// WindowInsets.statusBars.getTop(this).toDp() -// } -// ) -// ) -// -// Spacer(Modifier.height(40.dp)) -// -// Image( -// painter = painterResource(id = R.drawable.logo_whiteback), -// contentDescription = null, -// modifier = Modifier.size(64.dp) -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// text = "비밀번호 재설정", -// fontSize = 22.sp, -// fontWeight = FontWeight.Bold, -// fontFamily = Paperlogy -// ) -// -// Spacer(Modifier.height(8.dp)) -// -// Text( -// text = "걱정 마세요! 이메일 주소를 입력해 주시면,\n임시 비밀번호를 보내드릴게요!", -// fontSize = 16.sp, -// fontFamily = Paperlogy, -// color = Color(0xFF87898F) -// ) -// -// Spacer(Modifier.height(32.dp)) -// -// OutlinedTextField( -// value = email, -// onValueChange = onEmailChange, -// placeholder = { -// Text( -// "이메일 주소를 입력해주세요.", -// fontSize = 14.sp, -// fontFamily = Paperlogy, -// color = Color(0xFFB7B9BF) -// ) -// }, -// singleLine = true, -// modifier = Modifier -// .fillMaxWidth() -// .background(Color.White, RoundedCornerShape(16.dp)) -// .border( -// 1.dp, -// Brush.horizontalGradient( -// listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) -// ), -// RoundedCornerShape(16.dp) -// ), -// colors = TextFieldDefaults.colors( -// focusedIndicatorColor = Color.Transparent, -// unfocusedIndicatorColor = Color.Transparent, -// focusedContainerColor = Color.Transparent, -// unfocusedContainerColor = Color.Transparent -// ) -// ) -// -// if (error != null) { -// Spacer(Modifier.height(8.dp)) -// Text( -// text = error, -// color = Color(0xFFFF3B30), -// fontSize = 12.sp, -// fontFamily = Paperlogy, -// modifier = Modifier.padding(start = 8.dp) -// ) -// } -// -// // ✅ 이제 여기서 weight 사용 가능 -// Spacer(modifier = Modifier.weight(1f)) -// -// Box( -// modifier = Modifier -// .fillMaxWidth() -// .height(48.dp) -// .padding(bottom = bottomPadding) -// .background( -// brush = Brush.horizontalGradient( -// if (isEmailValid) -// listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) -// else -// listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)) -// ), -// shape = RoundedCornerShape(18.dp) -// ) -// .clickable(enabled = isEmailValid && !loading) { -// keyboardController?.hide() -// focusManager.clearFocus() -// onSubmit() -// }, -// contentAlignment = Alignment.Center -// ) { -// if (loading) { -// CircularProgressIndicator( -// modifier = Modifier.size(20.dp), -// strokeWidth = 2.dp, -// color = Color.White -// ) -// } else { -// Text( -// "임시 비밀번호 받기", -// color = Color.White, -// fontFamily = Paperlogy, -// fontSize = 16.sp, -// fontWeight = FontWeight.Bold -// ) -// } -// } -// } -// } -//} -// diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpNicknameScreen.kt b/feature/login/src/main/java/com/example/login/auth/SignUpNicknameScreen.kt deleted file mode 100644 index 57ee9a8d..00000000 --- a/feature/login/src/main/java/com/example/login/auth/SignUpNicknameScreen.kt +++ /dev/null @@ -1,323 +0,0 @@ -package com.example.login.auth - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.example.login.R -import com.example.login.Paperlogy -import androidx.navigation.NavHostController -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.rememberNavController -import androidx.compose.foundation.clickable -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import com.example.login.ui.item.LoginTextField -import com.example.login.ui.item.StepIndicator - -@Composable -fun SignUpNicknameScreen( - navigator: NavHostController, - signUpViewModel: SignUpViewModel = hiltViewModel() -) { - var nickname by remember { mutableStateOf(signUpViewModel.nickname) } - - val isNicknameAvailable by signUpViewModel.isNicknameAvailable.collectAsState() - val nicknameMessage by signUpViewModel.nicknameMessage.collectAsState() - val isLoading by signUpViewModel.isLoading.collectAsState() - - val isNicknameValid = nickname.isNotBlank() && nickname.length <= 10 - - // ✅ 버튼 활성 조건 (EmailVerificationScreen의 isButtonEnabled와 동일한 느낌) - val isButtonEnabled = isNicknameValid && - (isNicknameAvailable != false) && // false만 비활성, null(미확인) 허용 - !isLoading - - Box(modifier = Modifier.fillMaxSize()) { - - // ✅ 추가: 이메일 인증 화면과 동일한 바텀 패딩 계산 - val density = LocalDensity.current - val imeBottomPx = WindowInsets.ime.getBottom(density) - val isImeVisible = imeBottomPx > 0 - - val bottomGapWhenIme = 4.dp // ✅ 키보드 보일 때 버튼-키보드 간격 - val bottomGapDefault = 16.dp // ✅ 평소 하단 간격(기존 .padding(bottom = 16.dp)와 동일) - //val navBottomDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } - //val extraNavPadding = if (isImeVisible) 0.dp else navBottomDp - val bottomPadding = (if (isImeVisible) bottomGapWhenIme else bottomGapDefault) //+ extraNavPadding - - - // 본문 - Column( - modifier = Modifier - .fillMaxSize() - .padding( - start = 20.dp, - end = 20.dp, - top = 52.dp, - // ✅ 버튼과 겹치지 않도록 여유(버튼 48 + 하단간격 24, 필요시 더 넉넉히) - bottom = 48.dp + 24.dp - ), - horizontalAlignment = Alignment.Start - ) { - StepIndicator( - currentStep = 2, - totalSteps = 3, - label = "프로필 설정" - ) - Spacer(Modifier.height(32.dp)) - - Text( - text = "사용하실 닉네임을\n입력해주세요", - fontSize = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Spacer(Modifier.height(32.dp)) - - LoginTextField( - value = nickname, - onValueChange = { - nickname = it - signUpViewModel.nickname = it - if (isNicknameValid) { - signUpViewModel.checkNickname() - } - }, - hint = "닉네임을 입력해주세요.", - modifier = Modifier.fillMaxWidth(), - - ) - - if (isNicknameAvailable == false) { - Text( - "중복된 닉네임 입니다.", - fontSize = 13.sp, - fontFamily = Paperlogy, - color = Color(0xFFFF5E5E) - ) - } - if (nicknameMessage == "서버 요청 실패") { - Text( - "서버 요청 실패", - fontSize = 13.sp, - fontFamily = Paperlogy, - color = Color(0xFFFF5E5E) - ) - } - - Spacer(Modifier.height(12.dp)) - - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp) - .background( - if (isNicknameValid) Color(0xFFCB59EB) else Color(0xFFD7D9DF), - RoundedCornerShape(4.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Check, - contentDescription = "조건 만족", - tint = Color.White, - modifier = Modifier.size(12.dp) - ) - } - Spacer(Modifier.width(4.dp)) - Text( - "국문/영문 10자 이하", - fontSize = 12.sp, - fontFamily = Paperlogy, - color = Color(0xFF757575) - ) - } - - Spacer(modifier = Modifier.weight(1f)) - } - - // ✅ 하단 고정 버튼 (EmailVerificationScreen과 동일한 방식) - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) // 항상 하단 - // .imePadding() // 키보드 올라오면 자동 위로 - //.navigationBarsPadding() // 제스처/내비 바 안전영역 - //.padding(start = 20.dp, end = 20.dp, bottom = 16.dp) - .padding(start = 20.dp, end = 20.dp, bottom = bottomPadding) - .height(48.dp) - .background( - brush = Brush.horizontalGradient( - colors = when { - isButtonEnabled -> listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) - else -> listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)) - } - ), - shape = RoundedCornerShape(18.dp) - ) - .clickable(enabled = isButtonEnabled) { - - // 다음 화면으로 이동 (등록하신 라우트 사용) - navigator.navigate("sign_up_gender") { - // 뒤로가기로 되돌아오게 하고 싶으면 이 옵션들 생략 - // 중복 스택 방지하고 싶으면 아래처럼 설정 가능 - launchSingleTop = true - } - }, - contentAlignment = Alignment.Center - ) { - Text( - text = "다음", - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - fontFamily = Paperlogy, - textAlign = TextAlign.Center - ) - } - } -} - - -@Preview(showBackground = true) -@Composable -fun SignUpNicknameScreenPreview() { - val fakeNavController = rememberNavController() - SignUpNicknameScreenPreviewOnly(navigator = fakeNavController) - //SignUpNicknameScreen(navigator = fakeNavController) -} - - -/** - * Preview 전용: ViewModel 없이 UI만 보여줌 - * 프리뷰 이슈로 아예 ui만 보여주는....여기는 ui가 중요하니까...요... - */ -@Composable -private fun SignUpNicknameScreenPreviewOnly(navigator: NavHostController) { - var nickname by remember { mutableStateOf("테스트닉네임") } - - val isNicknameValid = nickname.isNotBlank() && nickname.length <= 10 - val isNicknameAvailable = true - val isLoading = false - - val isButtonEnabled = isNicknameValid && - (isNicknameAvailable != false) && - !isLoading - - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = 20.dp, end = 20.dp, top = 52.dp, bottom = 72.dp), - horizontalAlignment = Alignment.Start - ) { - StepIndicator( - currentStep = 2, - totalSteps = 3, - label = "프로필 설정" - ) - Spacer(Modifier.height(36.dp)) - - Text( - text = "사용하실 닉네임을\n입력해주세요", - fontSize = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Spacer(Modifier.height(40.dp)) - - LoginTextField( - value = nickname, - onValueChange = { - nickname = it - }, - hint = "닉네임을 입력해주세요.", - modifier = Modifier.fillMaxWidth() - - ) - - Spacer(Modifier.height(15.dp)) - - Row( - modifier = Modifier - .padding(start = 12.dp), // 오른쪽으로 12dp 이동 - verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp) - .background( - if (isNicknameValid) Color(0xFFCB59EB) else Color(0xFFD7D9DF), - RoundedCornerShape(4.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Check, - contentDescription = "조건 만족", - tint = Color.White, - modifier = Modifier.size(12.dp) - ) - } - Spacer(Modifier.width(8.dp)) - Text( - "국문/영문 10자 이하", - fontSize = 12.sp, - fontFamily = Paperlogy, - color = Color(0xFF757575) - ) - } - - Spacer(modifier = Modifier.weight(1f)) - } - - // 하단 버튼 - Box( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(start = 20.dp, end = 20.dp, bottom = 16.dp) - .height(48.dp) - .background( - brush = Brush.horizontalGradient( - colors = if (isButtonEnabled) - listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) - else - listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)) - ), - shape = RoundedCornerShape(18.dp) - ), - contentAlignment = Alignment.Center - ) { - Text( - text = "다음", - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - fontFamily = Paperlogy - ) - } - } -} diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpPasswordScreen.kt b/feature/login/src/main/java/com/example/login/auth/SignUpPasswordScreen.kt deleted file mode 100644 index b6287b26..00000000 --- a/feature/login/src/main/java/com/example/login/auth/SignUpPasswordScreen.kt +++ /dev/null @@ -1,403 +0,0 @@ -package com.example.login.auth - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.example.login.R -import com.example.login.Paperlogy -import androidx.navigation.NavHostController -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.compose.foundation.clickable -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.navigation.compose.rememberNavController -import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.login.ui.item.BottomGradientButton -import com.example.login.ui.item.LoginTextField -import com.example.login.ui.item.StepIndicator - -@Composable -fun SignUpPasswordScreen( - navigator: NavHostController, - signUpViewModel: SignUpViewModel = hiltViewModel() -) { - BackHandler { - navigator.popBackStack() - } - - // 항상 이메일 인증으로 되돌리는 로직 - //TODO :여기서 되돌아가면, 기존 인증한 이메일 정보가 보이는게 맞는지? 다인언니한테 물어보기! - //TODO : 이메일 정보로 돌아가면, 기존 입력한 이메일 상태로 나오는게 맞는지 아니면 다시 재입력? -> 이건 언니에게 물어보고 추후 수정하기! - //TODO : 비밀번호의 경우, 눈 가리개? 로 입력을 보이거나, 안보이게 해야 하는건 아닌지 다인 언니에게 물어보기! -// fun goBackToEmailVerification() { -// val popped = navigator.popBackStack("email_verification", inclusive = false) -// if (!popped) { -// navigator.navigate("email_verification") { -// launchSingleTop = true -// // 이미 존재하면 그 지점까지 스택 정리 (없으면 no-op) -// popUpTo("email_verification") { -// inclusive = false -// saveState = true -// } -// } -// } -// } -// -// BackHandler(enabled = true) { -// // 이메일 화면으로 돌아가기 전에 '리셋 신호' 전달 -// navigator.previousBackStackEntry -// ?.savedStateHandle -// ?.set("reset_email_screen", true) -// -// val popped = navigator.popBackStack("email_verification", inclusive = false) -// if (!popped) { -// navigator.navigate("email_verification") { -// launchSingleTop = true -// popUpTo("email_verification") { -// inclusive = false -// saveState = true -// } -// } -// } -// } - - var password by remember { mutableStateOf(signUpViewModel.password) } - var confirmPassword by remember { mutableStateOf("") } - - val isPasswordLengthValid = password.length in 8..20 - val isPasswordComplex = - password.any { it.isDigit() } && password.any { it.isLetter() } && password.any { !it.isLetterOrDigit() } - - val isPasswordValid = isPasswordLengthValid && isPasswordComplex - val doPasswordsMatch = password == confirmPassword - val showConfirmField = isPasswordValid - val canProceed = isPasswordValid && doPasswordsMatch - - Box(modifier = Modifier.fillMaxSize()) { - - - // ✅ 추가: 이메일 화면과 동일한 바텀 패딩 계산 - val density = LocalDensity.current - val imeBottomPx = WindowInsets.ime.getBottom(density) - val isImeVisible = imeBottomPx > 0 - - val bottomGapWhenIme = 4.dp // ← 키보드 보일 때 간격(더 붙이고 싶으면 0.dp) - val bottomGapDefault = 16.dp // ← 기존 코드의 42dp 유지 - val navBottomDp = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } - val extraNavPadding = if (isImeVisible) 0.dp else navBottomDp - val bottomPadding = (if (isImeVisible) bottomGapWhenIme else bottomGapDefault) - - // 본문 - Column( - modifier = Modifier - .fillMaxSize() - .padding( - start = 20.dp, - end = 20.dp, - top = 52.dp, - // 버튼 영역만큼 여유 (48dp 높이 + 32dp 바텀 패딩 + 약간의 버퍼) - bottom = 48.dp + 24.dp - ), - horizontalAlignment = Alignment.Start - ) { - StepIndicator( - currentStep = 1, - totalSteps = 3, - label = "계정 정보" - ) - Spacer(Modifier.height(32.dp)) - - Text( - text = "사용하실 비밀번호를\n 입력해주세요", - fontSize = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Bold, - color = Color.Black, - textAlign = TextAlign.Start - ) - - Spacer(Modifier.height(32.dp)) - - // 비밀번호 입력 - Box( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .background( - brush = Brush.horizontalGradient( - colors = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) - ), - shape = RoundedCornerShape(16.dp) - ) - .padding(1.dp) - ) { - LoginTextField( - value = password, - onValueChange = { - password = it - signUpViewModel.password = it - }, - hint = "비밀번호를 입력해주세요.", - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) - } - - Spacer(Modifier.height(10.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() //기쥰을 화면으로 - .padding(start = 12.dp), // 전체 오른쪽 이동 - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp) - .background( - if (isPasswordComplex) Color(0xFFCB59EB) else Color(0xFFD7D9DF), - RoundedCornerShape(4.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Check, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(12.dp) - ) - } - Spacer(Modifier.width(8.dp)) // 체크박스 ↔ 텍스트 - Text( - "영문, 숫자, 특수기호 조합", - fontFamily = Paperlogy, - fontSize = 12.sp, - color = Color(0xFF757575) - ) - } - - Spacer(Modifier.width(24.dp)) // 조건 간 간격 - - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp) - .background( - if (isPasswordLengthValid) Color(0xFFCB59EB) else Color(0xFFD7D9DF), - RoundedCornerShape(4.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Check, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(12.dp) - ) - } - Spacer(Modifier.width(8.dp)) // 체크박스 ↔ 텍스트 - Text( - "8~20자", - fontFamily = Paperlogy, - fontSize = 12.sp, - color = Color(0xFF757575) - ) - } - } - - if (showConfirmField) { - Spacer(Modifier.height(20.dp)) - LoginTextField( - value = confirmPassword, - onValueChange = { confirmPassword = it }, - hint = "비밀번호를 확인해주세요.", - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) - - if (confirmPassword.isNotEmpty() && !doPasswordsMatch) { - Text( - text = "비밀번호가 일치하지 않습니다. 다시 입력해주세요.", - fontSize = 13.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Normal, - color = Color(0xFFFF5E5E), - modifier = Modifier.padding(start = 8.dp, top = 4.dp) - ) - } - } - } - - // 하단 고정 버튼 (이메일 화면과 동일 위치) - BottomGradientButton( - text = "다음", - enabled = canProceed, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), - onClick = { - signUpViewModel.password = password - navigator.navigate("sign_up_nickname") - }, - modifier = Modifier.align(Alignment.BottomCenter) - ) - } -} - -@Composable -fun SignUpPasswordScreenContent( - password: String, - confirmPassword: String, - onPasswordChange: (String) -> Unit, - onConfirmPasswordChange: (String) -> Unit, - canProceed: Boolean, - isPasswordComplex: Boolean, - isPasswordLengthValid: Boolean, - doPasswordsMatch: Boolean, - bottomPadding: Dp, - onNext: () -> Unit -) { - Box(modifier = Modifier.fillMaxSize()) { - - Column( - modifier = Modifier - .fillMaxSize() - .padding( - start = 20.dp, - end = 20.dp, - top = 52.dp, - bottom = 48.dp + 24.dp - ) - ) { - StepIndicator( - currentStep = 1, - totalSteps = 3, - label = "계정 정보" - ) - - Spacer(Modifier.height(36.dp)) - - Text( - text = "사용하실 비밀번호를\n 입력해주세요", - fontSize = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Bold - ) - - Spacer(Modifier.height(32.dp)) - - LoginTextField( - value = confirmPassword, - onValueChange = onConfirmPasswordChange, - hint = "비밀번호를 확인해주세요.", - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) - Spacer(Modifier.height(12.dp)) - - // 조건 표시 - Row( - modifier = Modifier.padding(start = 12.dp), - verticalAlignment = Alignment.CenterVertically) { - PasswordRule("영문, 숫자, 특수기호 조합", isPasswordComplex) - Spacer(Modifier.width(12.dp)) - PasswordRule("8~20자", isPasswordLengthValid) - } - - if (password.length >= 8) { - Spacer(Modifier.height(20.dp)) - LoginTextField( - value = password, - onValueChange = onPasswordChange, - hint = "비밀번호를 입력해주세요.", - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - ) - - if (confirmPassword.isNotEmpty() && !doPasswordsMatch) { - Text( - text = "비밀번호가 일치하지 않습니다.", - fontSize = 13.sp, - fontFamily = Paperlogy, - color = Color(0xFFFF5E5E), - modifier = Modifier.padding(start = 8.dp, top = 4.dp) - ) - } - } - } - - // 하단 버튼 - BottomGradientButton( - text = "다음", - enabled = canProceed, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), - onClick = onNext, - modifier = Modifier.align(Alignment.BottomCenter) - ) - - } -} - -@Composable -private fun PasswordRule(text: String, satisfied: Boolean) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp) - .background( - if (satisfied) Color(0xFFCB59EB) else Color(0xFFD7D9DF), - RoundedCornerShape(8.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Default.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(12.dp)) - } - Spacer(Modifier.width(4.dp)) - Text(text, fontSize = 12.sp, fontFamily = Paperlogy, color = Color(0xFF757575)) - } -} - - - - -@Preview(showBackground = true) -@Composable -fun SignUpPasswordScreenContentPreview() { - SignUpPasswordScreenContent( - password = "Test@1234", - confirmPassword = "Test@1234", - onPasswordChange = {}, - onConfirmPasswordChange = {}, - canProceed = true, - isPasswordComplex = true, - isPasswordLengthValid = true, - doPasswordsMatch = true, - bottomPadding = 16.dp, - onNext = {} - ) -} diff --git a/feature/login/src/main/java/com/example/login/auth/TermsAgreementContent.kt b/feature/login/src/main/java/com/example/login/auth/TermsAgreementContent.kt deleted file mode 100644 index 5341ee68..00000000 --- a/feature/login/src/main/java/com/example/login/auth/TermsAgreementContent.kt +++ /dev/null @@ -1,185 +0,0 @@ -//package com.example.login.auth -// -//import androidx.compose.animation.EnterTransition -//import androidx.compose.animation.ExitTransition -//import androidx.compose.animation.core.tween -//import androidx.compose.foundation.background -//import androidx.compose.foundation.clickable -//import androidx.compose.foundation.interaction.MutableInteractionSource -//import androidx.compose.foundation.layout.* -//import androidx.compose.foundation.shape.RoundedCornerShape -//import androidx.compose.material.icons.Icons -//import androidx.compose.material.icons.filled.KeyboardArrowRight -//import androidx.compose.material3.* -//import androidx.compose.runtime.* -//import androidx.compose.runtime.saveable.rememberSaveable -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.draw.clip -//import androidx.compose.ui.graphics.Brush -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.platform.LocalDensity -//import androidx.compose.ui.text.font.FontWeight -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.compose.ui.unit.sp -//import androidx.hilt.navigation.compose.hiltViewModel -//import androidx.lifecycle.compose.collectAsStateWithLifecycle -//import androidx.navigation.NavController -//import com.example.login.R -//import androidx.compose.ui.graphics.Shape -//import androidx.compose.ui.platform.LocalConfiguration -//import com.example.login.Paperlogy -//import com.example.login.ui.bottom_sheet.NoAnimBottomSheet -//import com.example.login.ui.content.TermsAgreementContent -//import com.example.login.ui.item.AgreementItem -// -// -// -// -//@Preview(showBackground = true, name = "약관 동의 - 기본(모두 해제)") -//@Composable -//fun TermsAgreementContentPreview_Default() { -// TermsAgreementContent( -// agreeTerms = false, -// agreePrivacy = false, -// agreeMarketing = false, -// onAgreeTermsChange = {}, -// onAgreePrivacyChange = {}, -// onAgreeMarketingChange = {}, -// onClickTerms = {}, -// onClickPrivacy = {}, -// onClickMarketing = {}, -// onNextClicked = { _, _, _ -> } -// ) -//} -// -//@Preview(showBackground = true, name = "약관 동의 - 필수만 체크") -//@Composable -//fun TermsAgreementContentPreview_RequiredOnly() { -// TermsAgreementContent( -// agreeTerms = true, -// agreePrivacy = true, -// agreeMarketing = false, -// onAgreeTermsChange = {}, -// onAgreePrivacyChange = {}, -// onAgreeMarketingChange = {}, -// onClickTerms = {}, -// onClickPrivacy = {}, -// onClickMarketing = {}, -// onNextClicked = { _, _, _ -> } -// ) -//} -// -//@Preview(showBackground = true, name = "약관 동의 - 전체 동의") -//@Composable -//fun TermsAgreementContentPreview_AllChecked() { -// TermsAgreementContent( -// agreeTerms = true, -// agreePrivacy = true, -// agreeMarketing = true, -// onAgreeTermsChange = {}, -// onAgreePrivacyChange = {}, -// onAgreeMarketingChange = {}, -// onClickTerms = {}, -// onClickPrivacy = {}, -// onClickMarketing = {}, -// onNextClicked = { _, _, _ -> } -// ) -//} -// -// -// -// -//@OptIn(ExperimentalMaterial3Api::class) -//@Composable -//fun TermsAgreementSheet( -// navController: NavController, -// vm: SignUpViewModel, -// visible: Boolean, -// onClose: () -> Unit, -// onClickTerms: () -> Unit, -// onClickPrivacy: () -> Unit, -// onClickMarketing: () -> Unit -//) { -// if (!visible) return -// -// -// NoAnimBottomSheet( -// visible = visible, -// onDismissRequest = onClose, -// scrimColor = Color.Black.copy(alpha = 0.12f), -// shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) -// ) { -// -// val agreeTerms by vm.agreeTerms.collectAsStateWithLifecycle() -// val agreePrivacy by vm.agreePrivacy.collectAsStateWithLifecycle() -// val agreeMarketing by vm.agreeMarketing.collectAsStateWithLifecycle() -// -// TermsAgreementContent( -// agreeTerms = agreeTerms, -// agreePrivacy = agreePrivacy, -// agreeMarketing = agreeMarketing, -// onAgreeTermsChange = vm::setAgreeTerms, -// onAgreePrivacyChange = vm::setAgreePrivacy, -// onAgreeMarketingChange = vm::setAgreeMarketing, -// onClickTerms = onClickTerms, -// onClickPrivacy = onClickPrivacy, -// onClickMarketing = onClickMarketing, -// onNextClicked = { t, p, _ -> -// if (t && p) { -// onClose() -// navController.navigate("email_verification") { -// launchSingleTop = true -// } -// } -// } -// ) -// } -//} -// -//@Preview( -// showBackground = true, -// name = "Terms Agreement BottomSheet - 실제 화면", -// -//) -//@Composable -//fun TermsAgreementBottomSheetPreview() { -// -// // 🔹 Preview용 상태 -// var visible by remember { mutableStateOf(true) } -// var agreeTerms by remember { mutableStateOf(false) } -// var agreePrivacy by remember { mutableStateOf(false) } -// var agreeMarketing by remember { mutableStateOf(false) } -// -// Box(modifier = Modifier.fillMaxSize()) { -// -// // 🔹 실제 화면 배경 흉내 (뒤에 있는 화면) -// Box( -// modifier = Modifier -// .fillMaxSize() -// .background(Color(0xFFF5F6F9)) -// ) -// -// // 🔹 실제 쓰는 BottomSheet 그대로 -// NoAnimBottomSheet( -// visible = visible, -// onDismissRequest = { visible = false }, -// scrimColor = Color.Black.copy(alpha = 0.12f), -// shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) -// ) { -// TermsAgreementContent( -// agreeTerms = agreeTerms, -// agreePrivacy = agreePrivacy, -// agreeMarketing = agreeMarketing, -// onAgreeTermsChange = { agreeTerms = it }, -// onAgreePrivacyChange = { agreePrivacy = it }, -// onAgreeMarketingChange = { agreeMarketing = it }, -// onClickTerms = {}, -// onClickPrivacy = {}, -// onClickMarketing = {}, -// onNextClicked = { _, _, _ -> } -// ) -// } -// } -//} diff --git a/feature/login/src/main/java/com/example/login/auth/PasswordResetAlert.kt b/feature/login/src/main/java/com/example/login/ui/alert/PasswordResetAlert.kt similarity index 78% rename from feature/login/src/main/java/com/example/login/auth/PasswordResetAlert.kt rename to feature/login/src/main/java/com/example/login/ui/alert/PasswordResetAlert.kt index 3bb2f923..e4cdc4c5 100644 --- a/feature/login/src/main/java/com/example/login/auth/PasswordResetAlert.kt +++ b/feature/login/src/main/java/com/example/login/ui/alert/PasswordResetAlert.kt @@ -1,4 +1,4 @@ -package com.example.login.auth +package com.example.login.ui.alert import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -20,7 +20,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy +import com.example.design.theme.LocalColorTheme import com.example.login.R @Composable @@ -28,13 +29,18 @@ fun PasswordResetAlert( onDismissRequest: () -> Unit = {}, onConfirmClick: () -> Unit = {} ) { + // 현재 디자인 테마의 컬러 스킴 가져오기 + val colorTheme = LocalColorTheme.current + val paperlogyFamily = Paperlogy.font + + Dialog(onDismissRequest = onDismissRequest) { Column( modifier = Modifier .width(372.dp) .height(246.dp) .background( - color = Color(0xFFFFFFFF), + color = colorTheme.white, shape = RoundedCornerShape(22.dp) ) .padding(horizontal = 28.dp), @@ -57,31 +63,30 @@ fun PasswordResetAlert( // 🔹 로고 ↔ 타이틀 간격 23 Spacer(modifier = Modifier.height(23.dp)) - // 🔹 타이틀 + // 타이틀 Text( text = "비밀번호 재설정 메일 전송 완료!", style = TextStyle( fontSize = 18.sp, lineHeight = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight(500), - color = Color(0xFF000208), + fontFamily = paperlogyFamily, // 수정됨 + fontWeight = FontWeight.Medium, // 500은 Medium입니다. + color = colorTheme.black, textAlign = TextAlign.Center ) ) - // 🔹 타이틀 ↔ 설명 간격 20 Spacer(modifier = Modifier.height(20.dp)) - // 🔹 설명 텍스트 + // 설명 텍스트 Text( text = "비밀번호 재설정 메일을 발송했습니다.\n메일함을 확인해주세요!", style = TextStyle( fontSize = 15.sp, lineHeight = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight(400), - color = Color(0xFF87898F), + fontFamily = paperlogyFamily, // 수정됨 + fontWeight = FontWeight.Normal, // 400은 Normal입니다. + color = colorTheme.gray[600], textAlign = TextAlign.Center ) ) @@ -94,12 +99,7 @@ fun PasswordResetAlert( .width(316.dp) .height(50.dp) .background( - brush = Brush.horizontalGradient( - colors = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ) - ), + brush = colorTheme.maincolor, shape = RoundedCornerShape(14.dp) ) .clickable { onConfirmClick() }, @@ -110,9 +110,9 @@ fun PasswordResetAlert( style = TextStyle( fontSize = 16.sp, lineHeight = 20.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight(700), - color = Color(0xFFFFFFFF), + fontFamily = paperlogyFamily, // 수정됨 + fontWeight = FontWeight.Bold, // 700은 Bold입니다. + color = colorTheme.white, textAlign = TextAlign.Center ) ) diff --git a/feature/login/src/main/java/com/example/login/auth/AnimatedLoginScreen.kt b/feature/login/src/main/java/com/example/login/ui/animation/AnimatedLoginScreen.kt similarity index 61% rename from feature/login/src/main/java/com/example/login/auth/AnimatedLoginScreen.kt rename to feature/login/src/main/java/com/example/login/ui/animation/AnimatedLoginScreen.kt index 870c8020..4ef5f1b4 100644 --- a/feature/login/src/main/java/com/example/login/auth/AnimatedLoginScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/animation/AnimatedLoginScreen.kt @@ -1,13 +1,13 @@ -package com.example.login.auth +package com.example.login.ui.animation import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale @@ -20,13 +20,32 @@ import androidx.navigation.NavHostController import com.example.login.R import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.math.roundToInt +import com.example.design.util.DesignSystemBars +import androidx.compose.ui.graphics.Color +import com.example.login.LoginScreen +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens @Composable fun AnimatedLoginScreen( navigator: NavHostController, - onSignUpClick: () -> Unit + onSignUpClick: () -> Unit, + skipAnimation: Boolean = false ) { + + // 1. 디자인 모듈 컬러 테마 가져오기 + val colorTheme = LocalColorTheme.current + + // 2. 반응형 유틸리티 가져오기 + val (w, h) = rememberFigmaDimens() + + //로그인 진입 애니메이션도 바텀바 보이니 않도록 설정함. + DesignSystemBars( + statusBarColor = colorTheme.white, // 디자인 모듈의 white 사용 + navigationBarColor = colorTheme.white, + darkIcons = true, //배경이 화이트이므로 아이콘은 어두운 색(검정)으로 표시(시계, 배터리) + immersive = true + ) var hasAnimated by rememberSaveable { mutableStateOf(false) } val logoOffsetY = remember { Animatable(0f) } @@ -34,15 +53,34 @@ fun AnimatedLoginScreen( val contentAlpha = remember { Animatable(0f) } val density = LocalDensity.current + + // 피그마 기준 해상도(917)를 반영한 높이 계산 val screenHeightPx = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } val splashCenterY = screenHeightPx / 2f + + //기존 228f 수치를 유지하되 h() 유틸의 계산 방식과 정렬되도록 관리 val loginLogoY = screenHeightPx * (228f / 917f) val startOffsetY = splashCenterY - loginLogoY - LaunchedEffect(Unit) { + LaunchedEffect(skipAnimation) { + // 이메일 인증에서 돌아온 경우 → 애니메이션 완전 스킵 + if (skipAnimation) { + logoOffsetY.snapTo(0f) + logoAlpha.snapTo(1f) + contentAlpha.snapTo(1f) + + // 한 번 쓰고 바로 제거함. + navigator.currentBackStackEntry + ?.savedStateHandle + ?.remove("skip_login_animation") + + return@LaunchedEffect + } + + // 이미 한 번 애니메이션 했던 경우 if (hasAnimated) { logoOffsetY.snapTo(0f) logoAlpha.snapTo(1f) @@ -63,7 +101,7 @@ fun AnimatedLoginScreen( targetValue = 0f, animationSpec = tween( durationMillis = 1100, // ⭐ 길게 - easing = androidx.compose.animation.core.LinearOutSlowInEasing + easing = LinearOutSlowInEasing ) ) } @@ -75,7 +113,7 @@ fun AnimatedLoginScreen( 1f, tween( durationMillis = 500, - easing = androidx.compose.animation.core.LinearEasing + easing = LinearEasing ) ) } @@ -101,8 +139,8 @@ fun AnimatedLoginScreen( painter = painterResource(R.drawable.img_login_logo), contentDescription = "Login Logo", modifier = Modifier - .width(150.dp) - .height(106.dp), + .width(w(150f)) // 로고 너비 반응형 적용 (150dp -> w(150f)) + .height(h(106f)), contentScale = ContentScale.Fit ) } diff --git a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt index 24b9327e..edf2d41e 100644 --- a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt +++ b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens //약관 보고 다시 바텀 시트 돌아올 때,애니메이션 작동하지 않게 하는...시트 @@ -21,14 +23,24 @@ fun NoAnimBottomSheet( onDismissRequest: () -> Unit, scrimColor: Color = Color.Black.copy(alpha = 0.12f), shape: Shape, - containerColor: Color = Color.White, + containerColor: Color? = null, // 기본 컨테이너 컬러를 디자인 모듈의 white로 변경 content: @Composable ColumnScope.() -> Unit ) { if (!visible) return - Box(modifier = Modifier.fillMaxSize()) { + // 현재 디자인 테마의 컬러 스킴 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() - // Scrim + // 파라미터로 받은 컬러가 없으면 테마의 white 사용 + val finalContainerColor = containerColor ?: colorTheme.white + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter // 중앙 정렬 - 테블릿 가로 모드 대응. + ) { + + // Scrim(배경 어둡게) Box( modifier = Modifier .fillMaxSize() @@ -44,18 +56,18 @@ fun NoAnimBottomSheet( // BottomSheet body (NO animation) Surface( modifier = Modifier - .align(Alignment.BottomCenter) + .widthIn(max = 600.dp) // 태블릿 가로 모드에서 무한정 늘어남 방지 .fillMaxWidth() .wrapContentHeight() .imePadding(), shape = shape, - color = containerColor, + color = finalContainerColor, shadowElevation = 12.dp ) { Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 12.dp) + .padding(vertical = h(12f)) // 세로 태딩 반응형 적용함. ) { content() } diff --git a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/TermsAgreementSheet.kt b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/TermsAgreementSheet.kt index 656e6da0..fb9b61ff 100644 --- a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/TermsAgreementSheet.kt +++ b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/TermsAgreementSheet.kt @@ -5,14 +5,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import com.example.login.auth.SignUpViewModel +import com.example.login.viewmodel.SignUpViewModel import com.example.login.ui.content.TermsAgreementContent @@ -32,6 +31,8 @@ fun TermsAgreementSheet( ) { if (!visible) return + //바텀시트 떠 있을 때, 백버튼 = 시트 닫기 + NoAnimBottomSheet( visible = visible, onDismissRequest = onClose, diff --git a/feature/login/src/main/java/com/example/login/ui/content/TermsAgreementContent.kt b/feature/login/src/main/java/com/example/login/ui/content/TermsAgreementContent.kt index 357ee929..13f0b6df 100644 --- a/feature/login/src/main/java/com/example/login/ui/content/TermsAgreementContent.kt +++ b/feature/login/src/main/java/com/example/login/ui/content/TermsAgreementContent.kt @@ -21,12 +21,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import com.example.login.ui.item.AgreementItem import com.example.login.ui.item.GradientButtonCore import androidx.compose.foundation.Image import androidx.compose.ui.res.painterResource import com.example.design.R +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens @Composable @@ -42,6 +44,11 @@ fun TermsAgreementContent( onClickMarketing: () -> Unit, onNextClicked: (Boolean, Boolean, Boolean) -> Unit ) { + // 1. 테마 및 반응형 유틸 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + val agreeAll = agreeTerms && agreePrivacy && agreeMarketing val nextEnabled = agreeTerms && agreePrivacy @@ -51,7 +58,7 @@ fun TermsAgreementContent( // 바텀시트 실제 높이 기준 val topPadding = maxHeight * (46f / 280f) - Box(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier.fillMaxWidth() .background(Color.White)) { Column { @@ -61,24 +68,24 @@ fun TermsAgreementContent( modifier = Modifier .fillMaxWidth() .padding( - start = 32.dp, - end = 32.dp, - top = 36.dp + start = w(32f), + end = w(32f), + top = h(36f) //top = topPadding 화면으로 계산이 되어서 일단 보류. ) ) { // 커스텀 체크박스 Box( modifier = Modifier - .size(22.dp) + .size(w(22f)) // 반응형 적용 .border( width = 1.dp, - color = if (agreeAll) Color(0xFFD35EFF) else Color(0xFFD7D9DF), - shape = RoundedCornerShape(6.dp) + color = if (agreeAll) Color(0xFFD35EFF) else colorTheme.gray[300]!!, + shape = RoundedCornerShape(w(6f)) //반응형 적용 ) .background( - color = if (agreeAll) Color(0xFFD35EFF) else Color.White, - shape = RoundedCornerShape(6.dp) + color = if (agreeAll) colorTheme.purple[200]!! else colorTheme.white, + shape = RoundedCornerShape(w(6f)) //반응형 적용 ) .clickable { val checked = !agreeAll @@ -93,21 +100,21 @@ fun TermsAgreementContent( painter = painterResource(id = R.drawable.ic_checkbox_checked), contentDescription = null, modifier = Modifier - .width(10.5.dp) - .height(8.dp) + .width(w(11f)) + .height(h(8f)) //반응형으로 수정. ) } } - Spacer(Modifier.width(15.dp)) + Spacer(Modifier.width(w(15f))) Text( text = "약관 전체동의", fontSize = 18.sp, lineHeight = 22.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Medium, - color = Color(0xFF000208) + color = colorTheme.black ) Spacer(Modifier.width(8.dp)) @@ -115,8 +122,10 @@ fun TermsAgreementContent( Text( text = "선택항목에 대한 동의 포함", fontSize = 12.sp, - fontFamily = Paperlogy, - color = Color(0xFF87898F) + lineHeight = 14.sp, + fontWeight = FontWeight(400), + fontFamily = paperlogyFamily, + color = colorTheme.gray[600]!! ) } @@ -124,22 +133,22 @@ fun TermsAgreementContent( /* ───── Divider (좌우 20) ───── */ Divider( - color = Color(0xFFE5E5E5), + color = Color(0xFFD4E1FF), modifier = Modifier - .padding(horizontal = 20.dp, vertical = 16.dp) + .padding(horizontal = w(20f), vertical = h(16f)) ) /* ───── 약관 항목들 (좌우 32, 간격 25) ───── */ Column( - modifier = Modifier.padding(horizontal = 32.dp), - verticalArrangement = Arrangement.spacedBy(18.dp) + modifier = Modifier.padding(horizontal = w(32f)), + verticalArrangement = Arrangement.spacedBy(h(18f)) ) { AgreementItem( title = "이용약관", suffix = "(필수)", - suffixColor = Color(0xFF2C6FFF), + suffixColor = colorTheme.blue[200]!!, checked = agreeTerms, onCheckedChange = onAgreeTermsChange, onRowClick = onClickTerms @@ -148,7 +157,7 @@ fun TermsAgreementContent( AgreementItem( title = "개인정보 처리방침", suffix = "(필수)", - suffixColor = Color(0xFF2C6FFF), + suffixColor = colorTheme.blue[200]!!, checked = agreePrivacy, onCheckedChange = onAgreePrivacyChange, onRowClick = onClickPrivacy @@ -157,27 +166,21 @@ fun TermsAgreementContent( AgreementItem( title = "마케팅 수신 동의", suffix = "(선택)", - suffixColor = Color(0xFFB7B9BF), + suffixColor = colorTheme.gray[400]!!, checked = agreeMarketing, onCheckedChange = onAgreeMarketingChange, onRowClick = onClickMarketing ) } - Spacer(Modifier.height(30.dp)) + Spacer(Modifier.height(h(30f))) /* ───── 다음 버튼 (좌우 31) ───── */ GradientButtonCore( text = "다음", enabled = nextEnabled, - activeGradient = listOf( - Color(0xFF4D5FFF), - Color(0xFFA032F5) - ), - inactiveGradient = listOf( - Color(0xFFE1D6F9), - Color(0xFFF3E7FB) - ), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { onNextClicked(agreeTerms, agreePrivacy, agreeMarketing) }, diff --git a/feature/login/src/main/java/com/example/login/ui/item/AgreementItem.kt b/feature/login/src/main/java/com/example/login/ui/item/AgreementItem.kt index b28d5c91..63b6f623 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/AgreementItem.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/AgreementItem.kt @@ -19,9 +19,11 @@ 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 com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.foundation.Image import androidx.compose.ui.res.painterResource +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens import com.example.design.R as DesignR //약관 동의 3세트 @@ -34,6 +36,12 @@ fun AgreementItem( onCheckedChange: (Boolean) -> Unit, onRowClick: () -> Unit ) { + + // 1. 테마 및 반응형 유틸리티 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -44,15 +52,15 @@ fun AgreementItem( // 체크박스 Box( modifier = Modifier - .size(22.dp) + .size(w(22f)) .border( width = 1.dp, - color = if (checked) Color(0xFFD35EFF) else Color(0xFFD7D9DF), - shape = RoundedCornerShape(6.dp) + color = if (checked) colorTheme.purple[200]!! else colorTheme.gray[300]!!, + shape = RoundedCornerShape(w(6f)) ) .background( - color = if (checked) Color(0xFFD35EFF) else Color.White, - shape = RoundedCornerShape(6.dp) + color = if (checked) colorTheme.purple[200]!! else colorTheme.white, + shape = RoundedCornerShape(w(6f)) ) .clickable { onCheckedChange(!checked) }, contentAlignment = Alignment.Center @@ -61,15 +69,15 @@ fun AgreementItem( Icon( imageVector = Icons.Default.Check, contentDescription = null, - tint = Color.White, - modifier = Modifier.size(14.dp) + tint = colorTheme.white, + modifier = Modifier.size(w(14f)) ) } } - Spacer(Modifier.width(15.dp)) + Spacer(Modifier.width(w(15f))) - // ⭐ 텍스트 영역 (아이콘을 밀어내는 핵심) + // 텍스트 영역 (아이콘을 밀어내는 핵심) Row( modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically @@ -78,17 +86,18 @@ fun AgreementItem( text = title, fontSize = 16.sp, lineHeight = 22.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Normal + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Normal, + color = colorTheme.black ) - Spacer(Modifier.width(5.dp)) + Spacer(Modifier.width(w(5f))) Text( text = suffix, fontSize = 12.sp, lineHeight = 14.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(400), color = suffixColor ) @@ -99,8 +108,8 @@ fun AgreementItem( painter = painterResource(id = DesignR.drawable.ic_right), contentDescription = null, modifier = Modifier - .width(8.dp) - .height(13.dp) + .width(w(8f)) + .height(h(13f)) ) } } diff --git a/feature/login/src/main/java/com/example/login/ui/item/BackIconButton.kt b/feature/login/src/main/java/com/example/login/ui/item/BackIconButton.kt index d63312d1..24efc4e4 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/BackIconButton.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/BackIconButton.kt @@ -21,7 +21,7 @@ fun BackIconButton( contentDescription = "Back", modifier = modifier .width(10.dp) - .height(16.25.dp) + .height(18.dp) .clickable { onClick() }, contentScale = ContentScale.Fit ) diff --git a/feature/login/src/main/java/com/example/login/ui/item/BottomGradientButton.kt b/feature/login/src/main/java/com/example/login/ui/item/BottomGradientButton.kt index 4b18a3d9..7b7f1587 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/BottomGradientButton.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/BottomGradientButton.kt @@ -18,36 +18,47 @@ import androidx.compose.ui.unit.sp import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens + + //GradientButtonCore에서 순수 버튼 ui를 받아온 뒤, 여기서는 회원가입 중 //바닥에 있는 패딩 위치, 패딩 조절을 담당함. @Composable fun BottomGradientButton( text: String, enabled: Boolean, - activeGradient: List, - inactiveGradient: List, + activeGradient: Brush, + inactiveGradient: Brush, onClick: () -> Unit, modifier: Modifier = Modifier ) { val density = LocalDensity.current + + // 🔑 반응형 유틸리티 가져오기 + val (w, h) = rememberFigmaDimens() + val imeBottom = WindowInsets.ime.getBottom(density) val navBottom = WindowInsets.navigationBars.getBottom(density) + // 🔑 화면 높이 기준 (피그마 917) + val screenHeight = LocalConfiguration.current.screenHeightDp.dp val bottomPadding = when { imeBottom > 0 -> 20.dp // 키보드 열림 - navBottom > 0 -> 16.dp // 네비게이션 바 - else -> 24.dp // 풀스크린 - } + navBottom > 0 -> screenHeight *(16f/917f) // 네비게이션 바 없는 경우(반응형으로 수정) + else -> screenHeight *(24f/917f) //기본 안드로이드 바텀바 있는 경우(반응형으로 수정) + } Box( modifier = modifier .fillMaxWidth() .padding( - start = 20.dp, - end = 20.dp, + start = w(20f), + end = w(20f), bottom = bottomPadding ) ) { @@ -64,6 +75,10 @@ fun BottomGradientButton( @Preview(showBackground = true) @Composable private fun BottomGradientButtonEnabledPreview() { + // 🔑 디자인 모듈의 컬러를 직접 생성하거나 테마로 감싸서 확인 + val activeBrush = Brush.horizontalGradient(listOf(Color(0xFF2C6FFF), Color(0xFFC800FF))) + val inactiveBrush = Brush.horizontalGradient(listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF))) + Box( modifier = Modifier .fillMaxSize() @@ -73,8 +88,8 @@ private fun BottomGradientButtonEnabledPreview() { BottomGradientButton( text = "인증하기", enabled = true, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), + activeGradient = activeBrush, + inactiveGradient = inactiveBrush, onClick = {} ) } @@ -83,6 +98,9 @@ private fun BottomGradientButtonEnabledPreview() { @Preview(showBackground = true) @Composable private fun BottomGradientButtonDisabledPreview() { + val activeBrush = Brush.horizontalGradient(listOf(Color(0xFF2C6FFF), Color(0xFFC800FF))) + val inactiveBrush = Brush.horizontalGradient(listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF))) + Box( modifier = Modifier .fillMaxSize() @@ -92,10 +110,9 @@ private fun BottomGradientButtonDisabledPreview() { BottomGradientButton( text = "인증메일 발송", enabled = false, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), + activeGradient = activeBrush, + inactiveGradient = inactiveBrush, onClick = {} ) } -} - +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/ui/item/CheckIndicator.kt b/feature/login/src/main/java/com/example/login/ui/item/CheckIndicator.kt new file mode 100644 index 00000000..321869de --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/item/CheckIndicator.kt @@ -0,0 +1,62 @@ +package com.example.login.ui.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +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.unit.dp +import com.example.login.R +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun CheckIndicator( + checked: Boolean, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(18.dp) + .background( + color = if (checked) Color(0xFFCB59EB) else Color(0xFFD7D9DF), + shape = RoundedCornerShape(5.dp) + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_login_check), + contentDescription = if (checked) "checked" else "unchecked", + modifier = Modifier + .width(11.dp) //실제 기기상 작아보여 피그마 대비 사이즈를 키움. + .height(8.dp) + ) + + } +} + + + +@Preview( + name = "CheckIndicator - States", + showBackground = true +) +@Composable +private fun CheckIndicatorPreview() { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + CheckIndicator(checked = true) // 보라 배경 + 체크 + CheckIndicator(checked = false) // 회색 배경 + 체크 + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/ui/item/CircleItem.kt b/feature/login/src/main/java/com/example/login/ui/item/CircleItem.kt index d9564f8d..d2d04269 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/CircleItem.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/CircleItem.kt @@ -17,8 +17,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy + +// 리펙토링하면서 없어질 item @Composable fun CircleItem( emoji: String? = null, @@ -36,6 +38,9 @@ fun CircleItem( Color(0xFFC800FF) ) ) { + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + Box( modifier = modifier .size(sizeDp.dp) @@ -62,7 +67,7 @@ fun CircleItem( if (emoji != null) { Text( text = emoji, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontSize = 24.sp ) } @@ -70,7 +75,7 @@ fun CircleItem( Text( text = text, fontSize = 14.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, textAlign = TextAlign.Center, color = if (selected) Color.White else Color.Black ) diff --git a/feature/login/src/main/java/com/example/login/ui/item/GradientButtonCore.kt b/feature/login/src/main/java/com/example/login/ui/item/GradientButtonCore.kt index 756dc04e..e04c4102 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/GradientButtonCore.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/GradientButtonCore.kt @@ -18,26 +18,30 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens //여백 없는 순수 로그인 버튼 코어 @Composable fun GradientButtonCore( text: String, enabled: Boolean, - activeGradient: List, - inactiveGradient: List, + activeGradient: Brush, + inactiveGradient: Brush, onClick: () -> Unit, modifier: Modifier = Modifier ) { + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() // 반응형 적용 + val paperlogyFamily = Paperlogy.font Box( modifier = modifier .fillMaxWidth() - .height(50.dp) + .height(h(50f)) // 🔑 높이 반응형 (50.dp -> h(50f)) .background( - brush = Brush.horizontalGradient( - colors = if (enabled) activeGradient else inactiveGradient - ), + brush = if (enabled) activeGradient else inactiveGradient, shape = RoundedCornerShape(18.dp) ) .clickable(enabled = enabled, onClick = onClick), @@ -45,11 +49,11 @@ fun GradientButtonCore( ) { Text( text = text, - color = Color.White, + color = colorTheme.white, fontSize = 16.sp, lineHeight = 20.sp, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, textAlign = TextAlign.Center ) } @@ -60,8 +64,14 @@ fun GradientButtonCore( showBackground = true, name = "GradientButtonCore - Enabled" ) +@Preview( + showBackground = true, + name = "GradientButtonCore - Enabled" +) @Composable private fun GradientButtonCoreEnabledPreview() { + // 실제 앱에서는 LocalColorTheme이 주입되지만, + // 프리뷰에서는 수동으로 Brush를 생성하거나 테마로 감싸서 확인합니다. Box( modifier = Modifier .fillMaxWidth() @@ -71,13 +81,12 @@ private fun GradientButtonCoreEnabledPreview() { GradientButtonCore( text = "로그인하기", enabled = true, - activeGradient = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) + // List 대신 Brush를 직접 생성하여 전달 + activeGradient = Brush.horizontalGradient( + listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) ), - inactiveGradient = listOf( - Color(0xFF9BCBFF), - Color(0xFFF4AFFF) + inactiveGradient = Brush.horizontalGradient( + listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)) ), onClick = {} ) @@ -99,16 +108,14 @@ private fun GradientButtonCoreDisabledPreview() { GradientButtonCore( text = "로그인하기", enabled = false, - activeGradient = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) + // List 대신 Brush를 직접 생성하여 전달 + activeGradient = Brush.horizontalGradient( + listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) ), - inactiveGradient = listOf( - Color(0xFF9BCBFF), - Color(0xFFF4AFFF) + inactiveGradient = Brush.horizontalGradient( + listOf(Color(0xFFD4E1FF), Color(0xFFF2CCFF)) ), onClick = {} ) } } - diff --git a/feature/login/src/main/java/com/example/login/ui/item/LoginTextField.kt b/feature/login/src/main/java/com/example/login/ui/item/LoginTextField.kt index 4afa2183..db509259 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/LoginTextField.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/LoginTextField.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview @@ -32,6 +32,9 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens //회원가입 중 입력 텍스트 필드 @Composable @@ -43,19 +46,30 @@ fun LoginTextField( textStyle: TextStyle? = null, modifier: Modifier = Modifier ) { + + // 디자인 모듈 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + val shape = RoundedCornerShape(16.dp) val strokeWidth = 1.dp + val strokeWidthPx = with(LocalDensity.current) { strokeWidth.toPx() } Box( modifier = modifier - .height(56.dp) + .height(h(56f)) .drawBehind { + // 선의 절반 두께만큼 안쪽으로 좌표를 오프셋 시킴(좌우 테두리 잘림 방지) + val inset = strokeWidthPx / 2 drawRoundRect( - brush = Brush.horizontalGradient( - listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ) + brush = colorTheme.maincolor, + // 시작점 보정 + topLeft = androidx.compose.ui.geometry.Offset(inset, inset), + // 크기 보정 (양쪽 inset만큼 줄여야 함) + size = androidx.compose.ui.geometry.Size( + width = size.width - strokeWidthPx, + height = size.height - strokeWidthPx ), cornerRadius = CornerRadius(16.dp.toPx()), style = Stroke(width = strokeWidth.toPx()) @@ -73,100 +87,47 @@ fun LoginTextField( fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, - color = Color(0xFFB7B9BF) + fontFamily = paperlogyFamily, + color = colorTheme.gray[400]!! ) }, textStyle = textStyle ?: TextStyle( fontSize = 14.sp, - fontFamily = Paperlogy, - fontWeight = FontWeight.Normal + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Normal, + color = colorTheme.black ), singleLine = true, enabled = enabled, - modifier = Modifier .fillMaxSize() - .background(Color.White, shape), + .background(colorTheme.white, shape), shape = shape, colors = TextFieldDefaults.colors( + //테두리 제어 focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + + // 배경색 제어 focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + + // 비활성 상태의 텍스트/힌트 컬러 보정 (너무 흐려지지 않게) + disabledTextColor = colorTheme.black, + disabledPlaceholderColor = colorTheme.gray[400]!!, + ) ) } } -//@Composable -//fun LoginTextField( -// value: String, -// onValueChange: (String) -> Unit, -// hint: String, -// enabled: Boolean = true, -// modifier: Modifier = Modifier -//) { -// val shape = RoundedCornerShape(16.dp) -// -// Box( -// modifier = modifier -// //.fillMaxWidth() -// .height(56.dp) -// .background( -// brush = Brush.horizontalGradient( -// listOf( -// Color(0xFF2C6FFF), -// Color(0xFFC800FF) -// ) -// ), -// shape = shape -// ) -// .padding(1.dp) // 테두리 두께 -// ) { -// OutlinedTextField( -// value = value, -// onValueChange = onValueChange, -// -// placeholder = { -// Text( -// text = hint, -// fontSize = 14.sp, -// lineHeight = 20.sp, -// fontWeight = FontWeight.Medium, -// fontFamily = Paperlogy, -// color = Color(0xFFB7B9BF) -// ) -// }, -// -// textStyle = TextStyle( -// fontSize = 14.sp, -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Normal -// ), -// -// singleLine = true, -// enabled = enabled, -// -// modifier = Modifier -// .fillMaxSize() -// .background(Color.White, shape), -// -// shape = shape, -// -// colors = TextFieldDefaults.colors( -// focusedIndicatorColor = Color.Transparent, -// unfocusedIndicatorColor = Color.Transparent, -// focusedContainerColor = Color.Transparent, -// unfocusedContainerColor = Color.Transparent -// ) -// ) -// } -//} + @Preview( name = "LoginTextField Preview", @@ -175,12 +136,14 @@ fun LoginTextField( ) @Composable fun LoginTextFieldPreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() val text = remember { mutableStateOf("") } Column( modifier = Modifier - //.fillMaxWidth() - .padding(16.dp) + .background(colorTheme.gray[100]!!) + .padding(w(16f)) ) { // 그라데이션 테두리 ON LoginTextField( @@ -189,7 +152,7 @@ fun LoginTextFieldPreview() { hint = "이메일을 입력해주세요" ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(h(16f))) // 그라데이션 테두리 OFF LoginTextField( diff --git a/feature/login/src/main/java/com/example/login/ui/item/OptionButton.kt b/feature/login/src/main/java/com/example/login/ui/item/OptionButton.kt index 2b3f4303..4ea704ca 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/OptionButton.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/OptionButton.kt @@ -22,8 +22,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.design.R -import com.example.login.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.theme.font.Paperlogy +import com.example.design.util.rememberFigmaDimens //젠더, 직업 선택 버튼 @@ -33,8 +34,12 @@ fun OptionButton( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, - height: Dp = 56.dp + height: Dp = 54.dp ) { + + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() // 반응형 유틸 가져오기 + val paperlogyFamily = Paperlogy.font val shape = RoundedCornerShape(18.dp) val activeBorderGradient = listOf( @@ -53,11 +58,11 @@ fun OptionButton( if (selected) { Modifier .fillMaxWidth() - .height(54.dp) + .height(h(54f)) } else { Modifier - .width(372.dp) - .height(54.dp) + .width(w(372f)) + .height(h(54f)) } ) .clip(shape) @@ -69,7 +74,7 @@ fun OptionButton( ) } else { Modifier.background( - color = Color.White, + color = colorTheme.white, shape = shape ) } @@ -80,12 +85,12 @@ fun OptionButton( Brush.horizontalGradient(activeBorderGradient) else Brush.linearGradient( - listOf(Color(0xFFB7B9BF), Color(0xFFB7B9BF)) + listOf(colorTheme.gray[400]!!, colorTheme.gray[400]!!) ), shape = shape ) .clickable(onClick = onClick) - .padding(horizontal = 22.dp), // ⭐ 핵심 + .padding(horizontal = w(22f)), contentAlignment = Alignment.CenterStart ){ Row( @@ -97,36 +102,17 @@ fun OptionButton( text = text, fontSize = 15.sp, lineHeight = 22.sp, // 요구사항 반영 - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Normal, - color = if (selected) { - Color.Black - } else { - Color(0xFFA1A3A9) - } + color = if (selected) colorTheme.black else colorTheme.gray[500]!! ) // 선택된 경우만 체크 표시 if (selected) { - Box( - modifier = Modifier - .width(20.dp) - .height(20.dp) - .background( - color = Color(0xFFCB59EB), - shape = RoundedCornerShape(6.dp) - ), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(id = R.drawable.ic_checkbox_checked), - contentDescription = "선택됨", - modifier = Modifier - .padding(1.5.dp) - .width(9.54546.dp) - .height(7.27273.dp) - ) - } + CheckIndicator( + checked = selected, + modifier = Modifier.size(w(20f)) + ) } } } @@ -134,15 +120,18 @@ fun OptionButton( @Preview( showBackground = true, - backgroundColor = 0xFFF5F6F9, name = "OptionButton - 비활성" ) @Composable private fun OptionButtonPreview_Unselected() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .background(colorTheme.gray[100]!!) + .padding(w(16f)) // 프리뷰 패딩 반응형 적용 ) { OptionButton( text = "남성", @@ -154,15 +143,18 @@ private fun OptionButtonPreview_Unselected() { @Preview( showBackground = true, - backgroundColor = 0xFFF5F6F9, name = "OptionButton - 활성" ) @Composable private fun OptionButtonPreview_Selected() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .background(colorTheme.gray[100]!!) + .padding(w(16f)) // 프리뷰 패딩 반응형 적용 ) { OptionButton( text = "여성", diff --git a/feature/login/src/main/java/com/example/login/ui/item/PasswordLoginTextField.kt b/feature/login/src/main/java/com/example/login/ui/item/PasswordLoginTextField.kt index fdc48e7b..99a93a59 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/PasswordLoginTextField.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/PasswordLoginTextField.kt @@ -19,7 +19,7 @@ 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 com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import com.example.login.R import androidx.compose.foundation.Image import androidx.compose.ui.draw.drawBehind @@ -28,12 +28,15 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.tooling.preview.Preview +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens @Composable fun PasswordLoginTextField( @@ -43,10 +46,16 @@ fun PasswordLoginTextField( enabled: Boolean = true, modifier: Modifier = Modifier ) { + //디자인 모듈. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + val shape = RoundedCornerShape(16.dp) val strokeWidth = 1.dp // LoginTextField와 동일 + val strokeWidthPx = with(LocalDensity.current) { strokeWidth.toPx() } var isPasswordVisible by remember { mutableStateOf(false) } - + // value 파라미터와 동기화된 TextFieldValue 관리 var fieldValue by remember { mutableStateOf(TextFieldValue(text = value)) } @@ -54,14 +63,15 @@ fun PasswordLoginTextField( Box( modifier = modifier .fillMaxWidth() - .height(56.dp) + .height(h(56f)) .drawBehind { + val inset = strokeWidthPx / 2 drawRoundRect( - brush = Brush.horizontalGradient( - listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ) + brush = colorTheme.maincolor, + topLeft = androidx.compose.ui.geometry.Offset(inset, inset), + size = androidx.compose.ui.geometry.Size( + width = size.width - strokeWidthPx, + height = size.height - strokeWidthPx ), cornerRadius = CornerRadius(16.dp.toPx()), style = Stroke(width = strokeWidth.toPx()) @@ -82,8 +92,8 @@ fun PasswordLoginTextField( Text( text = hint, fontSize = 14.sp, - fontFamily = Paperlogy, - color = Color(0xFFB7B9BF) + fontFamily = paperlogyFamily, + color = colorTheme.gray[400]!! ) }, @@ -92,15 +102,15 @@ fun PasswordLoginTextField( TextStyle( fontSize = 14.sp, lineHeight = 16.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(500), - color = Color(0xFF000208), + color = colorTheme.black, letterSpacing = 0.sp ) } else { TextStyle( fontSize = 14.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, letterSpacing = 2.sp ) @@ -117,8 +127,8 @@ fun PasswordLoginTextField( modifier = Modifier .fillMaxSize() - .background(Color.White, shape) - .padding(end = 40.dp), // 👁 아이콘 공간 + .background(colorTheme.white, shape) + .padding(end = w(40f)),// 👁 아이콘 공간 shape = shape, @@ -142,8 +152,8 @@ fun PasswordLoginTextField( contentDescription = null, modifier = Modifier .align(Alignment.CenterEnd) - .padding(end = 18.dp) - .size(22.dp) + .padding(end = w(18f)) + .size(w(22f)) .clickable { isPasswordVisible = !isPasswordVisible } @@ -152,119 +162,6 @@ fun PasswordLoginTextField( } } - -//@Composable -//fun PasswordLoginTextField( -// value: String, -// onValueChange: (String) -> Unit, -// hint: String = "비밀번호", -// enabled: Boolean = true, -// modifier: Modifier = Modifier -//) { -// val shape = RoundedCornerShape(16.dp) -// var isPasswordVisible by remember { mutableStateOf(false) } -// -// var fieldValue by remember { -// mutableStateOf(TextFieldValue(text = value)) -// } -// -// Box( -// modifier = modifier -// .fillMaxWidth() -// .height(56.dp) -// .background( -// Brush.horizontalGradient( -// listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) -// ), -// shape -// ) -// .padding(1.dp) -// ) { -// Box { -// OutlinedTextField( -// value = fieldValue, -// onValueChange = { newValue -> -// val fixedValue = newValue.copy(composition = null) //밑줄 방지 -// fieldValue = fixedValue -// onValueChange(fixedValue.text) -// }, -// -// placeholder = { -// Text( -// text = hint, -// fontSize = 14.sp, -// fontFamily = Paperlogy, -// color = Color(0xFFB7B9BF) -// ) -// }, -// -// textStyle = -// if (isPasswordVisible) { -// // 👁 비밀번호 보임 상태 (일반 텍스트) -// TextStyle( -// fontSize = 14.sp, -// lineHeight = 16.sp, -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Medium, -// color = Color(0xFF000208), -// letterSpacing = 0.sp -// ) -// } else { -// // 비밀번호 숨김 상태 (닷) -// TextStyle( -// fontSize = 14.sp, -// fontFamily = Paperlogy, -// fontWeight = FontWeight.Bold, -// letterSpacing = 2.sp -// ) -// }, -// -// singleLine = true, -// enabled = enabled, -// -// visualTransformation = -// if (isPasswordVisible) -// VisualTransformation.None -// else -// DotPasswordVisualTransformation(), -// -// modifier = Modifier -// .fillMaxSize() -// .background(Color.White, shape) -// .padding(end = 40.dp), -// -// shape = shape, -// -// colors = TextFieldDefaults.colors( -// cursorColor = Color.Black, -// focusedIndicatorColor = Color.Transparent, -// unfocusedIndicatorColor = Color.Transparent, -// focusedContainerColor = Color.Transparent, -// unfocusedContainerColor = Color.Transparent -// ) -// ) -// -// // 👁 눈 아이콘 -// Image( -// painter = painterResource( -// if (isPasswordVisible) -// R.drawable.ic_password_visibility_on -// else -// R.drawable.ic_password_visibility_off -// ), -// contentDescription = null, -// modifier = Modifier -// .align(Alignment.CenterEnd) -// .padding(end = 18.dp) -// .size(22.dp) -// .clickable { -// isPasswordVisible = !isPasswordVisible -// } -// ) -// } -// } -//} - //커스텀 닷 class DotPasswordVisualTransformation : VisualTransformation { override fun filter(text: AnnotatedString): TransformedText { @@ -286,12 +183,15 @@ class DotPasswordVisualTransformation : VisualTransformation { ) @Composable private fun PasswordLoginTextFieldHiddenPreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() var password by remember { mutableStateOf("") } Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .background(colorTheme.gray[100]!!) + .padding(w(16f)) ) { PasswordLoginTextField( value = password, @@ -307,12 +207,15 @@ private fun PasswordLoginTextFieldHiddenPreview() { ) @Composable private fun PasswordLoginTextFieldVisiblePreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() var password by remember { mutableStateOf("password123") } Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .background(colorTheme.gray[100]!!) + .padding(w(16f)) ) { // 강제로 눈 열린 상태 확인용 CompositionLocalProvider { diff --git a/feature/login/src/main/java/com/example/login/ui/item/PasswordRuleItem.kt b/feature/login/src/main/java/com/example/login/ui/item/PasswordRuleItem.kt new file mode 100644 index 00000000..46030b1f --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/item/PasswordRuleItem.kt @@ -0,0 +1,81 @@ +package com.example.login.ui.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.design.theme.LocalColorTheme +import com.example.design.theme.font.Paperlogy +import com.example.design.util.rememberFigmaDimens +import com.example.login.R + + +//회원가입 로직에서 사용하는 체크박스(그 네모 박스에 체크 아이콘 있는거) +@Composable +fun PasswordRuleItem( + text: String, + satisfied: Boolean, + modifier: Modifier = Modifier +) { + + // 1. 테마 및 반응형 유틸리티 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + CheckIndicator(checked = satisfied) //체크박스(활성화/비활성화) + + Spacer(modifier = Modifier.width(w(8f))) + + Text( + text = text, + fontSize = 12.sp, + fontFamily = paperlogyFamily, + color = Color(0xFF757575) + ) + } +} + +//프리뷰 +@Preview( + name = "PasswordRuleItem - States", + showBackground = true +) +@Composable +private fun PasswordRuleItemPreview() { + val (w, h) = rememberFigmaDimens() + + Column( + modifier = Modifier.padding(w(16f)), + verticalArrangement = Arrangement.spacedBy(h(8f)) + ) { + PasswordRuleItem( + text = "영문자를 포함해야 해요", + satisfied = true + ) + + PasswordRuleItem( + text = "숫자를 포함해야 해요", + satisfied = false + ) + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/ui/item/ResetPasswordTopHeader.kt b/feature/login/src/main/java/com/example/login/ui/item/ResetPasswordTopHeader.kt index b84ec63b..dc941716 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/ResetPasswordTopHeader.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/ResetPasswordTopHeader.kt @@ -17,18 +17,22 @@ import com.example.design.modifier.noRippleClickable import androidx.compose.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens @Composable fun ResetPasswordTopHeader( onBack: () -> Unit, modifier: Modifier = Modifier ) { - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - val screenWidth = LocalConfiguration.current.screenWidthDp.dp - // 🔑 피그마: Y 위치 - val topOffset = screenHeight * (59f / 917f) - val startPadding = screenWidth * (20f / 412f) + // 1. 테마 및 반응형 유틸리티 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + + // 피그마 기준 해상도(412x917) 대비 반응형 좌표 계산 + val topOffset = h(59f) + val startPadding = w(20f) Box( modifier = modifier @@ -43,8 +47,8 @@ fun ResetPasswordTopHeader( painter = painterResource(id = R.drawable.ic_back_black), contentDescription = "뒤로가기", modifier = Modifier - .width(10.dp) - .height(16.25.dp) + .width(w(11f)) // 너비 반응형 적용 + .height(h(20f)) .noRippleClickable { onBack() } ) } @@ -52,11 +56,13 @@ fun ResetPasswordTopHeader( @Preview( name = "ResetPasswordTopHeader Preview", - showBackground = true, - backgroundColor = 0xFFFFFFFF + showBackground = true ) @Composable private fun ResetPasswordTopHeaderPreview() { + + val colorTheme = LocalColorTheme.current + Box( modifier = Modifier .fillMaxWidth() diff --git a/feature/login/src/main/java/com/example/login/ui/item/SocialLoginButton.kt b/feature/login/src/main/java/com/example/login/ui/item/SocialLoginButton.kt index f61e7abf..df397365 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/SocialLoginButton.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/SocialLoginButton.kt @@ -20,7 +20,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens import com.example.login.R @Composable @@ -33,11 +35,17 @@ fun SocialLoginButton( textColor: Color, onClick: () -> Unit = {} ) { + + // 1. 테마 및 반응형 유틸리티 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + Surface( modifier = modifier .fillMaxWidth() - .widthIn(max = 372.dp) // 반응형 + 피그마 최대 너비 - .height(50.dp), + .widthIn(max = w(372f)) // 너비 반응형 적용 + .height(h(50f)), color = backgroundColor, shape = RoundedCornerShape(18.dp), border = borderColor?.let { BorderStroke(1.dp, it) } @@ -50,7 +58,7 @@ fun SocialLoginButton( interactionSource = remember { MutableInteractionSource() }, onClick = onClick ) - .padding(horizontal = 18.dp), + .padding(horizontal = w(18f)), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { @@ -60,15 +68,15 @@ fun SocialLoginButton( Image( painter = painterResource(iconRes), contentDescription = null, - modifier = Modifier.size(22.dp) + modifier = Modifier.size(w(22f)) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(w(8f))) } Text( text = text, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontSize = 16.sp, lineHeight = 20.sp, fontWeight = FontWeight.Bold, @@ -81,36 +89,29 @@ fun SocialLoginButton( @Preview(showBackground = true) @Composable fun SocialLoginButtonPreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() Box( modifier = Modifier .fillMaxSize() // 프리뷰용 배경 (피그마 그라데이션) - .background( - brush = Brush.linearGradient( - colors = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ) - ) - ), + .background(brush = colorTheme.maincolor), contentAlignment = Alignment.Center ) { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp), // 좌우 여백 20 + .padding(horizontal = w(20f)), // 좌우 여백 20 horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) // 버튼 간격 10 + verticalArrangement = Arrangement.spacedBy(h(10f)) //반응형으로 변경.ㄴ ) { - - // 카카오 SocialLoginButton( backgroundColor = Color(0xFFFEE500), iconRes = R.drawable.icon_login_kakao, text = "카카오로 시작하기", - textColor = Color.Black + textColor = colorTheme.black ) // 네이버 @@ -118,16 +119,16 @@ fun SocialLoginButtonPreview() { backgroundColor = Color(0xFF03C75A), iconRes = R.drawable.icon_login_naver, text = "네이버로 시작하기", - textColor = Color.White + textColor = colorTheme.white ) // 구글 SocialLoginButton( - backgroundColor = Color.White, + backgroundColor = colorTheme.white, borderColor = Color(0xFFE0E0E0), iconRes = R.drawable.icon_login_google, text = "구글로 시작하기", - textColor = Color.Black + textColor = colorTheme.black ) // 이메일 diff --git a/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt b/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt index a42bc8c9..d111971c 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt @@ -14,7 +14,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.example.login.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.theme.font.Paperlogy +import com.example.design.util.rememberFigmaDimens @Composable fun StepIndicator( @@ -26,6 +28,16 @@ fun StepIndicator( inactiveColor: Color = Color(0xFFD6D6D6), completedColor: Color = Color(0xFFE5ACF4) ) { + + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + // 100% 일치하는 컬러만 토큰으로 매칭 + val finalActiveColor = colorTheme.purple[200] + val finalCompletedColor = colorTheme.purple[100] + val finalInactiveColor = inactiveColor + Column( modifier = modifier, horizontalAlignment = Alignment.Start @@ -66,7 +78,7 @@ fun StepIndicator( Text( text = step.toString(), fontSize = if (isStep3Current) 18.sp else 16.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, color = Color.White ) @@ -76,7 +88,7 @@ fun StepIndicator( Text( text = step.toString(), fontSize = 16.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, color = inactiveColor ) @@ -117,7 +129,7 @@ fun StepIndicator( ), fontSize = 13.sp, lineHeight = 15.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Light, color = activeColor, textAlign = TextAlign.Center diff --git a/feature/login/src/main/java/com/example/login/ui/screen/EmailLoginScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/EmailLoginScreen.kt new file mode 100644 index 00000000..0dfd1c20 --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/screen/EmailLoginScreen.kt @@ -0,0 +1,257 @@ +package com.example.login.ui.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +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 androidx.navigation.NavHostController +import com.example.login.R +import com.example.design.theme.font.Paperlogy +import com.example.login.ui.item.LoginTextField +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.BaselineShift +import com.example.login.ui.item.GradientButtonCore +import com.example.login.ui.item.PasswordLoginTextField +import com.example.design.modifier.noRippleClickable +import android.util.Patterns +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import com.example.design.theme.LocalColorTheme +import com.example.design.util.DesignSystemBars +import com.example.login.viewmodel.LoginViewModel +import com.example.design.util.rememberFigmaDimens + +@Composable +fun EmailLoginScreen( + navigator: NavHostController, + loginViewModel: LoginViewModel? = null, + onSignUpClick: () -> Unit +) { + + // 1. 키보드 제어를 위한 FocusManager 가져오기 + val focusManager = LocalFocusManager.current + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val colorTheme = LocalColorTheme.current + val paperlogyFamily = Paperlogy.font + + // Figma 412×917 기준 반응형 + val (w, h) = rememberFigmaDimens() + + // 로그인 입력 화면부터는 시스템 바 다시 표시 + DesignSystemBars( + statusBarColor = colorTheme.white, + navigationBarColor = colorTheme.white, + darkIcons = true, + immersive = false + ) + + + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + val isEmailValid = + Patterns.EMAIL_ADDRESS.matcher(email).matches() + val isFormValid = email.isNotBlank() && password.isNotBlank() && isEmailValid + + // 🔑 화면 높이 + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + + // 🔑 키보드 상태 (프리뷰 안전) + val density = LocalDensity.current + val isInPreview = LocalInspectionMode.current + val imeBottom = if (isInPreview) 0 else WindowInsets.ime.getBottom(density) + val isKeyboardOpen = imeBottom > 0 + val buttonOffsetY = if (isKeyboardOpen) 0.dp else h(-4f) + + // 🔑 BottomGradientButton 내부 padding과 동일한 값 계산 + val navBottom = WindowInsets.navigationBars.getBottom(density) + + + + // 🔑 피그마 비율 적용 + val logoRatio = if (isKeyboardOpen) 102f / 917f else 262f / 917f //키보드 활성화 전, 후 + val logoTopPadding = screenHeight * logoRatio + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + // 로고 위치 (비율 기반, 전체가 같이 이동) + Spacer(modifier = Modifier.height(logoTopPadding)) + + Image( + painter = painterResource(id = R.drawable.ic_logo_color), + contentDescription = "LinkU Logo", + modifier = Modifier + .width(w(84.6f)) + .height(h(60f)), + contentScale = ContentScale.Fit + ) + + + Spacer(Modifier.height(h(8f))) + + Text( + text = "Link U, Think You", + style = TextStyle( + fontSize = 13.sp, + lineHeight = 15.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight(400), + color = colorTheme.gray[600]!!, + textAlign = TextAlign.Center + ) + ) + + Spacer(Modifier.height(h(40f))) + + // 입력 영역 + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = w(20f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + LoginTextField( + value = email, + onValueChange = { email = it }, + hint = "이메일", + textStyle = TextStyle( + fontSize = 14.sp, + lineHeight = 16.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight(500), + color = colorTheme.black + ) + ) + + Spacer(Modifier.height(h(10f))) + + PasswordLoginTextField( + value = password, + onValueChange = { password = it } + ) + } + + Spacer(Modifier.height(h(45f))) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = w(20f)) + .offset(y = buttonOffsetY) + ) { + GradientButtonCore( + text = "로그인하기", + enabled = isFormValid, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = { + loginViewModel?.login( + email.trim(), + password.trim() + ) + } + ) + } + + Spacer(Modifier.height(h(20f))) + + + // 🔑 비율 기반 가로 위치 계산 + // 디자인 기준 너비 412 대비 현재 화면의 비율 지점 + val resetStartPos = w(101f) // 비밀번호 재설정 시작점 + val dividerStartPos = w(220f) // | 시작점 + val signUpStartPos = w(247f) // 회원가입 시작점 + + Box( + modifier = Modifier + .fillMaxWidth() + .height(h(30f)) // 클릭 영역 확보를 위한 높이 + ) { + // 1. 비밀번호 재설정 + Text( + text = "비밀번호 재설정", + fontSize = 15.sp, + fontFamily = paperlogyFamily, + color = Color(0xFF87898F), + modifier = Modifier + .offset(x = resetStartPos) // 항상 101/412 지점 + .noRippleClickable { + navigator.navigate("resetPassword") + } + ) + + // 2. 구분선 (|) + Text( + text = "|", + fontSize = 14.sp, + fontFamily = paperlogyFamily, + color = Color(0xFF87898F), + style = TextStyle(baselineShift = BaselineShift(0.15f)), + modifier = Modifier + .offset(x = dividerStartPos) // 항상 220/412 지점 + ) + + // 3. 회원가입 + Text( + text = "회원가입", + fontSize = 15.sp, + fontFamily = paperlogyFamily, + color = Color(0xFF87898F), + modifier = Modifier + .offset(x = signUpStartPos) // 항상 247/412 지점 + .noRippleClickable { + focusManager.clearFocus() + onSignUpClick() + } + ) + } + + } + } + } + + + +@Preview( + name = "Email Login – Keyboard OFF", + showBackground = true +) +@Composable +fun EmailLoginPreview() { + EmailLoginScreen( + navigator = rememberNavController(), + loginViewModel = null, + onSignUpClick = {} + ) +} + + + + diff --git a/feature/login/src/main/java/com/example/login/auth/EmailVerificationScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt similarity index 83% rename from feature/login/src/main/java/com/example/login/auth/EmailVerificationScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt index cc990571..70927751 100644 --- a/feature/login/src/main/java/com/example/login/auth/EmailVerificationScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt @@ -1,4 +1,4 @@ -package com.example.login.auth +package com.example.login.ui.screen import android.util.Patterns import android.widget.Toast @@ -7,7 +7,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* @@ -16,8 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration @@ -27,12 +24,16 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController import kotlinx.coroutines.delay import androidx.navigation.compose.rememberNavController -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry import com.example.login.ui.item.LoginTextField import com.example.login.ui.item.StepIndicator import com.example.login.ui.item.BottomGradientButton +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.EmailAuthViewModel +import com.example.login.viewmodel.SignUpViewModel +import com.example.design.theme.LocalColorTheme /** * 이메일 인증 화면의 UI와 로직을 담당하는 화면임. @@ -47,8 +48,7 @@ fun EmailVerificationScreen( signUpViewModel: SignUpViewModel = hiltViewModel() ) { - // 이메일 인증 화면은 뒤로가면 로그인 화면(약관 선택 페이지)으로 돌아가는게 맞는지 - //TODO : 다인언니에게 물어보기! -> 맞다고 함. + BackHandler { parentEntry.savedStateHandle["from_email_verification"] = true navigator.popBackStack() // ← 이게 정답 @@ -159,7 +159,9 @@ fun EmailVerificationScreen( } /** - * UI만 그리는 프레젠테이션 컴포저블입니다. Preview에서 ViewModel 없이 안전하게 사용 가능. 코드 아님. + * UI만 그리는 프레젠테이션 컴포저블입니다. + * Preview에서 ViewModel 없이 안전하게 사용 가능. + * 여기 이메일 인증에서는 """ui"""만 당당합니다 */ @Composable fun EmailVerificationScreenContent( @@ -181,11 +183,23 @@ fun EmailVerificationScreenContent( onSendCode: () -> Unit, onVerifyCode: () -> Unit ) { + + //디자인 모듈 불러오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() // Figma 412×917 기준 반응형 + val paperlogyFamily = Paperlogy.font + + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() - .padding(start = 20.dp, top = 52.dp, end = 20.dp, bottom = 48.dp + 24.dp), + .padding( + start = w(20f), + end = w(20f), + top = h(60f), + bottom = h(48f + 24f) + ), horizontalAlignment = Alignment.Start ) { //1단계 @@ -194,16 +208,16 @@ fun EmailVerificationScreenContent( totalSteps = 3, label = "계정 정보" ) - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(h(36f))) Text( text = "가입을 위한 이메일 주소를\n인증해주세요", fontSize = 22.sp, lineHeight = 30.sp, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, color = Color.Black ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(h(32f))) // 이메일 입력 필드 //이메일 입력 필드 @@ -211,7 +225,8 @@ fun EmailVerificationScreenContent( value = email, onValueChange = onEmailChange, hint = "이메일 주소를 입력해주세요", - enabled = true + enabled = !isCodeSent, // 인증번호 발송 후엔 수정 불가. //enabled = true + modifier = Modifier.fillMaxWidth(), ) // 에러 문구 @@ -221,54 +236,55 @@ fun EmailVerificationScreenContent( else -> null } emailErrorText?.let { - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(h(6f))) Text( text = it, color = Color(0xFFFF5E5E), fontSize = 13.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Medium, - modifier = Modifier.offset(x = 4.dp) + modifier = Modifier.offset( + x = w(4f) + ) + ) } // 인증 코드 입력 영역 이건 타이머가 있어서 따로 요소 불러오지 않고 여기서만. if (isCodeSent) { - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(h(10f))) OutlinedTextField( value = code, onValueChange = onCodeChange, placeholder = { Text( "코드를 입력해주세요", - fontSize = 13.sp, - fontFamily = Paperlogy, - color = Color(0xFF757575) + fontSize = 14.sp, + fontFamily = paperlogyFamily, + color = colorTheme.gray[400]!! ) }, modifier = Modifier .fillMaxWidth() - .height(56.dp) - .background(Color.White, shape = RoundedCornerShape(16.dp)) + .height(h(56f)) + .background(colorTheme.white, shape = RoundedCornerShape(16.dp)) .border( width = 1.dp, - brush = Brush.horizontalGradient( - colors = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) - ), + brush = colorTheme.maincolor, shape = RoundedCornerShape(16.dp) ), shape = RoundedCornerShape(16.dp), singleLine = true, enabled = !isVerifying, trailingIcon = { - val textModifier = Modifier.padding(end = 12.dp) + val textModifier = Modifier.padding(end = w(12f)) if (sendResult == "서버 오류") { Text( text = "서버 오류", color = Color(0xFFFF5E5E), fontSize = 13.sp, lineHeight = 15.sp, - fontFamily = Paperlogy, - modifier = Modifier.padding(end = 22.dp), + fontFamily = paperlogyFamily, + modifier = Modifier.padding(end = w(22f)), textAlign = TextAlign.Right ) } else { @@ -276,7 +292,7 @@ fun EmailVerificationScreenContent( text = timerText, color = Color(0xFFFF5E5E), fontSize = 13.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, modifier = textModifier ) } @@ -293,30 +309,32 @@ fun EmailVerificationScreenContent( verifyResult == "인증번호가 올바르지 않습니다" || verifyResult == "인증 코드 불일치" || verifyResult == "인증 실패" -> - "이메일 인증 코드가 잘못 입력되었습니다." + "이메일 인증 코드가 잘못 입력 되었습니다." else -> null } codeErrorText?.let { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(h(12f))) Text( text = it, color = Color(0xFFFF5E5E), fontSize = 13.sp, lineHeight = 15.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(400), - modifier = Modifier.padding(start = 12.dp) + modifier = Modifier.padding( + start = w(12f) + ) ) }//TODO : 하진 언니한테 오류 멘트 받아올 수 있는 api 수정 부탁하기! } else if (sendResult == "서버 오류") { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(h(8f))) Text( text = "서버 오류: 잠시 후 다시 시도해주세요", color = Color.Red, fontSize = 13.sp, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(h(8f)) ) } } @@ -335,13 +353,15 @@ fun EmailVerificationScreenContent( text = "인증번호가 오지 않는다면?", fontSize = 12.sp, lineHeight = 20.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight(500), - color = Color(0xFFB7B9BF), + color = colorTheme.gray[400]!!, textAlign = TextAlign.Center, textDecoration = TextDecoration.Underline, modifier = Modifier - .padding(bottom = 21.dp) + .padding( + bottom = h(21f) + ) .clickable { // TODO: 재전송 안내 api 개발시 연동하기! } @@ -350,8 +370,8 @@ fun EmailVerificationScreenContent( BottomGradientButton( text = if (isCodeSent) "인증하기" else "인증메일 발송", enabled = isButtonEnabled, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { if (isCodeSent) { onVerifyCode() diff --git a/feature/login/src/main/java/com/example/login/auth/InterestContentScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/InterestContentScreen.kt similarity index 89% rename from feature/login/src/main/java/com/example/login/auth/InterestContentScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/InterestContentScreen.kt index 6b2ab31b..be045238 100644 --- a/feature/login/src/main/java/com/example/login/auth/InterestContentScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/InterestContentScreen.kt @@ -1,33 +1,20 @@ -package com.example.login.auth +package com.example.login.ui.screen import CircleItem import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -37,16 +24,18 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.DpOffset import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.ui.unit.Dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.example.login.R +import com.example.design.theme.LocalColorTheme import com.example.login.ui.item.BottomGradientButton import com.example.login.ui.item.StepIndicator +import com.example.login.viewmodel.SignUpViewModel /** * 관심사 선택 화면의 버블 데이터 클래스 */ + +// ui 전면 변경 예정으로, 리펙토링 진행하지 않음.(수정 1월말~2월 초) data class Content(val emoji: String, val label: String, val size: Float, val offset: DpOffset) /** @@ -94,6 +83,11 @@ fun InterestContentScreen( navigator: NavHostController, signUpViewModel: SignUpViewModel? = null ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + val colorTheme = LocalColorTheme.current + val isPreview = LocalInspectionMode.current // ViewModel 기존 선택값 복원 (뒤로가기 해도 유지됨) val selectedContents = remember { @@ -121,14 +115,8 @@ fun InterestContentScreen( BottomGradientButton( text = "다음", enabled = canProceed, - activeGradient = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ), - inactiveGradient = listOf( - Color(0xFF9BCBFF), - Color(0xFFF4AFFF) - ), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { val codes = selectedContents .mapNotNull { contentLabelToCodeNormalized[normalizeLabel(it)] } @@ -183,7 +171,7 @@ fun InterestContentScreen( } }, fontSize = 22.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold ) diff --git a/feature/login/src/main/java/com/example/login/auth/InterestPurposeScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/InterestPurposeScreen.kt similarity index 90% rename from feature/login/src/main/java/com/example/login/auth/InterestPurposeScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/InterestPurposeScreen.kt index bdfcdb67..2ec98983 100644 --- a/feature/login/src/main/java/com/example/login/auth/InterestPurposeScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/InterestPurposeScreen.kt @@ -1,33 +1,20 @@ -package com.example.login.auth +package com.example.login.ui.screen import CircleItem import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -35,15 +22,17 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.DpOffset -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.ui.unit.Dp -import com.example.login.R +import com.example.design.theme.LocalColorTheme import com.example.login.ui.item.StepIndicator import com.example.login.ui.item.BottomGradientButton +import com.example.login.viewmodel.SignUpViewModel + +// ui 전면 변경 예정으로, 리펙토링 진행하지 않음.(수정 1월말~2월 초) //-------------------------------------------------------------------------- /** * 퍼포즈(저장 목적) 데이터 클래스 ─ 모든 좌표/크기 이모지 피그마 계측값 반영 @@ -165,6 +154,11 @@ fun InterestPurposeScreen( signUpViewModel: SignUpViewModel? = null //signUpViewModel: SignUpViewModel = hiltViewModel() // Preview에서는 null, 실제 앱에서는 Hilt로 주입 ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + val colorTheme = LocalColorTheme.current + val isPreview = LocalInspectionMode.current val selectedPurposes = remember { @@ -191,14 +185,8 @@ fun InterestPurposeScreen( BottomGradientButton( text = "다음", enabled = canProceed, - activeGradient = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ), - inactiveGradient = listOf( - Color(0xFF9BCBFF), - Color(0xFFF4AFFF) - ), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { val codes = selectedPurposes .mapNotNull { purposeLabelToCodeNormalized[normalizePurpose(it)] } @@ -244,6 +232,7 @@ fun InterestPurposeScreen( SpanStyle( color = Color(0xFFE5ACF4), fontSize = 16.sp, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Medium ) ) { @@ -252,7 +241,7 @@ fun InterestPurposeScreen( }, fontSize = 22.sp, lineHeight = 30.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 20.dp) ) diff --git a/feature/login/src/main/java/com/example/login/ui/screen/ResetPasswordScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/ResetPasswordScreen.kt new file mode 100644 index 00000000..963a2862 --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/screen/ResetPasswordScreen.kt @@ -0,0 +1,171 @@ +package com.example.login.ui.screen + +import android.util.Patterns +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.example.design.theme.font.Paperlogy +import com.example.login.R +import com.example.login.ui.item.BottomGradientButton +import com.example.login.ui.item.LoginTextField +import com.example.login.ui.item.ResetPasswordTopHeader +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.ResetPasswordViewModel +import com.example.design.theme.LocalColorTheme + +// ======================= +// 실제 Screen (ViewModel 사용) +// ======================= +@Composable +fun ResetPasswordScreen( + navigator: NavHostController, + viewModel: ResetPasswordViewModel? = hiltViewModel() +) { + //디자인 모듈 불러오기. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() // Figma 412×917 기준 반응형 + val paperlogyFamily = Paperlogy.font + + // 🔑 Preview면 viewModel == null + val ui = viewModel?.ui?.collectAsState()?.value + + var email by remember { mutableStateOf("test@email.com") } + + val isEmailValid = + Patterns.EMAIL_ADDRESS.matcher(email).matches() + + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { + ResetPasswordTopHeader( + onBack = { + if (viewModel != null) { + navigator.navigate("email_login") { + popUpTo("resetPassword") { inclusive = true } + } + } + } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = w(20f)), + horizontalAlignment = Alignment.Start + ) { + + // 헤더 밀기 + Spacer(modifier = Modifier.height(h(59f))) + + // 로고 위 여백 (38 / 917) + Spacer(modifier = Modifier.height(h(38f))) + + Image( + painter = painterResource(id = R.drawable.ic_logo_color), + contentDescription = null, + modifier = Modifier + .width(w(56.4f)) // 반응형 너비 + .height(h(40f)), // 반응형 높이 + contentScale = ContentScale.Fit + + ) + + // 로고-제목 간격 (18 / 917) + Spacer(modifier = Modifier.height(h(18f))) + + Text( + text = "비밀번호 재설정", + fontSize = 22.sp, + lineHeight = 30.sp, + fontWeight = FontWeight.Bold, + fontFamily = paperlogyFamily, + color = colorTheme.black + ) + + // 제목-설명 간격 (22 / 917) + Spacer(modifier = Modifier.height(h(22f))) + + Text( + text = "링큐에 가입했던 이메일을 입력해주세요. \n비밀번호를 다시 설정할 수 있는 메일을 보내드릴게요.", + fontSize = 16.sp, + lineHeight = 22.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight(400), + color = colorTheme.gray[600]!! + ) + + // 설명-입력창 간격 (45 / 917) + Spacer(modifier = Modifier.height(h(45f))) + + LoginTextField( + value = email, + onValueChange = { + email = it + if (ui?.error != null) viewModel?.consumeError() + }, + hint = "이메일 주소를 입력해주세요" + ) + + if (ui?.error != null) { + Spacer(Modifier.height(h(8f))) + Text( + text = ui.error, + color = Color(0xFFFF3B30), + fontSize = 12.sp, + fontFamily = paperlogyFamily, + modifier = Modifier.padding( + start = w(8f) + ) + + ) + } + + Spacer(modifier = Modifier.weight(1f)) + } + + BottomGradientButton( + text = "메일 보내기", + enabled = isEmailValid && (ui?.loading != true), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = { + keyboardController?.hide() + focusManager.clearFocus() + viewModel?.request(email) + }, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} + +// ======================= +// Preview (UI 확인 전용) +// ======================= +@Preview(showBackground = true, name = "ResetPassword UI Preview") +@Composable +fun ResetPasswordScreenPreview() { + ResetPasswordScreen( + navigator = rememberNavController(), + viewModel = null + ) +} + + diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpGenderScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpGenderScreen.kt similarity index 60% rename from feature/login/src/main/java/com/example/login/auth/SignUpGenderScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/SignUpGenderScreen.kt index 5a9c9622..2c12e603 100644 --- a/feature/login/src/main/java/com/example/login/auth/SignUpGenderScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpGenderScreen.kt @@ -1,23 +1,13 @@ -package com.example.login.auth +package com.example.login.ui.screen import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -26,41 +16,41 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import com.example.login.ui.item.BottomGradientButton import com.example.login.ui.item.StepIndicator import com.example.login.ui.item.OptionButton +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.SignUpViewModel +import com.example.design.theme.LocalColorTheme @Composable fun SignUpGenderScreen( navigator: NavHostController, signUpViewModel: SignUpViewModel = hiltViewModel() ) { + //디자인 모듈 불러오기. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() // Figma 412×917 기준 반응형 + val paperlogyFamily = Paperlogy.font + // 성별 선택 상태: 1 = 남성, 2 = 여성 var selectedGender by remember { mutableStateOf(signUpViewModel.gender) } //var selectedGender by remember { mutableStateOf(null) } val isButtonEnabled = selectedGender != null - Box(modifier = Modifier.fillMaxSize()) { - // ✅ 닉네임 화면과 동일한 바텀 패딩 계산 - val density = LocalDensity.current - val imeBottomPx = WindowInsets.ime.getBottom(density) - val isImeVisible = imeBottomPx > 0 - val bottomGapWhenIme = 4.dp // 키보드 보일 때 - val bottomGapDefault = 16.dp // 키보드 없을 때(시작 지점) - val bottomPadding = if (isImeVisible) bottomGapWhenIme else bottomGapDefault + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxSize() .padding( - start = 20.dp, - end = 20.dp, - top = 52.dp, // ⬆️ 위쪽만 52 - bottom = 48.dp + 24.dp // ⬇️ 아래는 40 유지 + start = w(20f), + end = w(20f), + top = h(60f), + bottom = h(72f) // 48 + 24 ), //.padding(horizontal = 20.dp, vertical = 40.dp), horizontalAlignment = Alignment.Start @@ -72,18 +62,18 @@ fun SignUpGenderScreen( label = "프로필 설정" ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(Modifier.height(h(32f))) Text( text = "성별을\n선택해주세요", fontSize = 22.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - color = Color.Black, + color = colorTheme.black, textAlign = TextAlign.Start ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(Modifier.height(h(36f))) // 선택 옵션: 남성 OptionButton( @@ -95,7 +85,7 @@ fun SignUpGenderScreen( } ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(h(10f))) // 선택 옵션: 여성 OptionButton( @@ -112,8 +102,8 @@ fun SignUpGenderScreen( BottomGradientButton( text = "다음", enabled = isButtonEnabled, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { signUpViewModel.gender = selectedGender ?: 1 navigator.navigate("sign_up_job") { @@ -124,32 +114,26 @@ fun SignUpGenderScreen( ) } } -//shape = RoundedCornerShape(18.dp) - -@Preview( - showBackground = true, - backgroundColor = 0xFFF5F6F9, - name = "성별 선택 - 선택된 버튼만" -) +@Preview(showBackground = true, name = "성별 선택 - 프리뷰") @Composable fun SignUpGenderScreenPreview() { - val fakeNavController = rememberNavController() - SignUpGenderScreenPreviewOnly(navigator = fakeNavController) -} - - -//철저히 프리뷰용. ui 확인용. -@Composable -private fun SignUpGenderScreenPreviewOnly(navigator: NavHostController) { - var selectedGender by remember { mutableStateOf(2) } // 테스트용, "여성" 선택 상태 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + var selectedGender by remember { mutableStateOf(2) } // 테스트용 여성 선택 val isButtonEnabled = selectedGender != null - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { Column( modifier = Modifier .fillMaxSize() - .padding(start = 20.dp, end = 20.dp, top = 52.dp, bottom = 72.dp), + .padding( + start = w(20f), + end = w(20f), + top = h(52f), + bottom = h(72f) + ), horizontalAlignment = Alignment.Start ) { StepIndicator( @@ -158,29 +142,27 @@ private fun SignUpGenderScreenPreviewOnly(navigator: NavHostController) { label = "프로필 설정" ) - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(h(36f))) Text( text = "성별을\n선택해주세요", fontSize = 22.sp, lineHeight = 30.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - color = Color.Black + color = colorTheme.black ) - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(h(40f))) - // 남성 버튼 OptionButton( text = "남성", selected = selectedGender == 1, onClick = { selectedGender = 1 } ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(h(12f))) - // 여성 버튼 OptionButton( text = "여성", selected = selectedGender == 2, @@ -190,13 +172,12 @@ private fun SignUpGenderScreenPreviewOnly(navigator: NavHostController) { Spacer(modifier = Modifier.weight(1f)) } - // 하단 버튼 BottomGradientButton( text = "다음", enabled = isButtonEnabled, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), - onClick = {}, // 프리뷰용: 동작 필요 없음 + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) } diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpJobScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpJobScreen.kt similarity index 62% rename from feature/login/src/main/java/com/example/login/auth/SignUpJobScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/SignUpJobScreen.kt index 242b883c..7ce50ea9 100644 --- a/feature/login/src/main/java/com/example/login/auth/SignUpJobScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpJobScreen.kt @@ -1,22 +1,14 @@ -package com.example.login.auth +package com.example.login.ui.screen import androidx.compose.foundation.background import androidx.compose.runtime.* -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -25,17 +17,25 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.theme.font.Paperlogy import com.example.login.ui.item.BottomGradientButton import com.example.login.ui.item.OptionButton import com.example.login.ui.item.StepIndicator +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.SignUpViewModel @Composable fun SignUpJobScreen( navigator: NavHostController, signUpViewModel: SignUpViewModel = hiltViewModel() ) { + //디자인 모듈 불러오기. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens()// Figma 412×917 기준 반응형 + val paperlogyFamily = Paperlogy.font + + var selectedJobIndex by remember { mutableStateOf( if (signUpViewModel.jobId > 0) signUpViewModel.jobId - 1 else null ) } @@ -44,25 +44,18 @@ fun SignUpJobScreen( Box(modifier = Modifier .fillMaxSize() - .background(Color.White)) { + .background(colorTheme.white)) { - // ✅ 닉네임/성별과 동일한 바텀 패딩 계산 - val density = LocalDensity.current - val imeBottomPx = WindowInsets.ime.getBottom(density) - val isImeVisible = imeBottomPx > 0 - val bottomGapWhenIme = 4.dp - val bottomGapDefault = 16.dp - val bottomPadding = if (isImeVisible) bottomGapWhenIme else bottomGapDefault // 본문 (버튼과 겹치지 않게 하단 여유 48+24) Column( modifier = Modifier .fillMaxSize() .padding( - start = 20.dp, - end = 20.dp, - top = 52.dp, - bottom = 48.dp + 24.dp + start = w(20f), + end = w(20f), + top = h(60f), + bottom = h(72f) // 48 + 24 ), horizontalAlignment = Alignment.Start ) { @@ -71,19 +64,19 @@ fun SignUpJobScreen( totalSteps = 3, label = "프로필 설정" ) - Spacer(modifier = Modifier.height(36.dp)) + Spacer(Modifier.height(h(36f))) Text( text = "현재 하고 계신 일이나\n활동을 알려주세요", fontSize = 22.sp, lineHeight = 30.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - color = Color.Black, + color = colorTheme.black, textAlign = TextAlign.Start ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(Modifier.height(h(32f))) jobs.forEachIndexed { index, job -> OptionButton( @@ -95,7 +88,7 @@ fun SignUpJobScreen( }, modifier = Modifier.fillMaxWidth() // 반응형 유지 ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(h(12f))) } Spacer(modifier = Modifier.weight(1f)) @@ -105,8 +98,8 @@ fun SignUpJobScreen( BottomGradientButton( text = "다음", enabled = isButtonEnabled, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, onClick = { signUpViewModel.jobId = (selectedJobIndex ?: 0) + 1 navigator.navigate("sign_up_purpose") { @@ -120,30 +113,29 @@ fun SignUpJobScreen( } -@Preview( - showBackground = true, - backgroundColor = 0xFFF5F6F9, - name = "직업 선택 전체 화면" -) -@Composable -fun SignUpJobScreenPreview() { - val fakeNavController = rememberNavController() - SignUpJobScreenPreviewOnly(navigator = fakeNavController) -} //ui 확인용. 철저히 프리뷰용. +@Preview(showBackground = true, name = "직업 선택 - 프리뷰") @Composable -private fun SignUpJobScreenPreviewOnly(navigator: NavHostController) { - var selectedJobIndex by remember { mutableStateOf(2) } // "직장인" 선택 예시 +fun SignUpJobScreenPreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + var selectedJobIndex by remember { mutableStateOf(2) } val jobs = listOf("고등학생", "대학생", "직장인", "자영업자", "프리랜서", "취준생") - val isButtonEnabled = selectedJobIndex != null - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { Column( modifier = Modifier .fillMaxSize() - .padding(start = 20.dp, end = 20.dp, top = 52.dp, bottom = 72.dp), + .padding( + start = w(20f), + end = w(20f), + top = h(52f), + bottom = h(72f) + ), horizontalAlignment = Alignment.Start ) { StepIndicator( @@ -151,17 +143,18 @@ private fun SignUpJobScreenPreviewOnly(navigator: NavHostController) { totalSteps = 3, label = "프로필 설정" ) - Spacer(modifier = Modifier.height(32.dp)) + + Spacer(modifier = Modifier.height(h(32f))) Text( text = "현재 하고 계신 일이나\n활동을 알려주세요", fontSize = 22.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - color = Color.Black + color = colorTheme.black ) - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(h(40f))) jobs.forEachIndexed { index, job -> OptionButton( @@ -170,21 +163,19 @@ private fun SignUpJobScreenPreviewOnly(navigator: NavHostController) { onClick = { selectedJobIndex = index }, modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(h(12f))) } Spacer(modifier = Modifier.weight(1f)) } - //하단 버튼 BottomGradientButton( text = "다음", - enabled = isButtonEnabled, - activeGradient = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)), - inactiveGradient = listOf(Color(0xFF9BCBFF), Color(0xFFF4AFFF)), - onClick = {}, // 프리뷰에서는 동작 필요 없음 + enabled = selectedJobIndex != null, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) } -} - +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt new file mode 100644 index 00000000..d10476ba --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt @@ -0,0 +1,211 @@ +package com.example.login.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.design.theme.font.Paperlogy +import androidx.navigation.NavHostController +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.rememberNavController +import androidx.compose.runtime.* +import com.example.design.theme.LocalColorTheme +import com.example.login.ui.item.BottomGradientButton +import com.example.login.ui.item.LoginTextField +import com.example.login.ui.item.PasswordRuleItem +import com.example.login.ui.item.StepIndicator +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.SignUpViewModel + +@Composable +fun SignUpNicknameScreen( + navigator: NavHostController, + signUpViewModel: SignUpViewModel = hiltViewModel() +) { + //디자인 모듈 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + var nickname by remember { mutableStateOf(signUpViewModel.nickname) } + + val isNicknameAvailable by signUpViewModel.isNicknameAvailable.collectAsState() + val nicknameMessage by signUpViewModel.nicknameMessage.collectAsState() + val isLoading by signUpViewModel.isLoading.collectAsState() + + + val isNicknameValid = nickname.isNotBlank() && nickname.length <= 6 //국문/영문 닉네임 글자수 6글자 이하로 제안 + + // 버튼 활성 조건 (EmailVerificationScreen의 isButtonEnabled와 동일한 느낌) + val isButtonEnabled = isNicknameValid && + (isNicknameAvailable != false) && // false만 비활성, null(미확인) 허용 + !isLoading + + Box(modifier = Modifier.fillMaxSize()) { + + // 본문 + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = w(20f), + end = w(20f), + top = h(52f), + bottom = h(48f + 24f) + ), + horizontalAlignment = Alignment.Start + ) { + StepIndicator( + currentStep = 2, + totalSteps = 3, + label = "프로필 설정" + ) + Spacer(Modifier.height(h(32f))) + + Text( + text = "사용하실 닉네임을\n입력해주세요", + fontSize = 22.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Bold, + color = colorTheme.black + ) + + Spacer(Modifier.height(h(32f))) + + LoginTextField( + value = nickname, + onValueChange = { + nickname = it + signUpViewModel.nickname = it + if (isNicknameValid) { + signUpViewModel.checkNickname() + } + }, + hint = "닉네임을 입력해주세요.", + modifier = Modifier.fillMaxWidth(), + + ) + + if (isNicknameAvailable == false) { + Spacer(Modifier.height(h(6f))) + Text( + "중복된 닉네임 입니다.", + fontSize = 13.sp, + lineHeight = 15.sp, + fontWeight = FontWeight(400), + fontFamily = paperlogyFamily, + color = Color(0xFFFF5E5E) + ) + } + if (nicknameMessage == "서버 요청 실패") { + Spacer(Modifier.height(h(6f))) + Text( + "서버 요청 실패", + fontSize = 13.sp, + lineHeight = 15.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight(400), + color = Color(0xFFFF5E5E) + ) + } + + Spacer(Modifier.height(h(12f))) + + PasswordRuleItem( + text = "국문/영문 6자 이하", + satisfied = isNicknameValid, + modifier = Modifier.padding(start = w(32f)) + ) + + Spacer(modifier = Modifier.weight(1f)) + } + + // 하단 고정 버튼 (EmailVerificationScreen과 동일한 방식) + BottomGradientButton( + text = "다음", + enabled = isButtonEnabled, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = { + navigator.navigate("sign_up_gender") { + launchSingleTop = true + } + }, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun SignUpNicknameScreenPreview() { + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + var nickname by remember { mutableStateOf("LinkU") } + val isNicknameValid = nickname.isNotBlank() && nickname.length <= 6 + + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(start = w(20f), end = w(20f), top = h(52f), bottom = h(72f)), + horizontalAlignment = Alignment.Start + ) { + StepIndicator( + currentStep = 2, + totalSteps = 3, + label = "프로필 설정" + ) + + Spacer(Modifier.height(h(32f))) + + Text( + text = "사용하실 닉네임을\n입력해주세요", + fontSize = 22.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Bold, + color = colorTheme.black + ) + + Spacer(Modifier.height(h(32f))) + + LoginTextField( + value = nickname, + onValueChange = { nickname = it }, + hint = "닉네임을 입력해주세요.", + modifier = Modifier.fillMaxWidth() + ) + + Spacer(Modifier.height(h(15f))) + + PasswordRuleItem( + text = "국문/영문 6자 이하", + satisfied = isNicknameValid, + modifier = Modifier.padding(start = w(12f)) + ) + + Spacer(modifier = Modifier.weight(1f)) + } + + BottomGradientButton( + text = "다음", + enabled = isNicknameValid, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = {}, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt new file mode 100644 index 00000000..af11c856 --- /dev/null +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt @@ -0,0 +1,283 @@ +package com.example.login.ui.screen + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.design.theme.font.Paperlogy +import androidx.navigation.NavHostController +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.ui.unit.Dp +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens +import com.example.login.ui.item.BottomGradientButton +import com.example.login.ui.item.LoginTextField +import com.example.login.ui.item.StepIndicator +import com.example.login.ui.item.PasswordRuleItem +import com.example.login.ui.item.PasswordLoginTextField +import com.example.login.viewmodel.SignUpViewModel + + +@Composable +fun SignUpPasswordScreen( + navigator: NavHostController, + signUpViewModel: SignUpViewModel = hiltViewModel() +) { + + //디자인 모듈 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + + BackHandler { navigator.popBackStack() } + + + var password by remember { mutableStateOf(signUpViewModel.password) } + var confirmPassword by remember { mutableStateOf("") } + + val isPasswordLengthValid = password.length in 8..20 + val isPasswordComplex = + password.any { it.isDigit() } && + password.any { it.isLetter() } && + password.any { !it.isLetterOrDigit() } + + val isPasswordValid = isPasswordLengthValid && isPasswordComplex + val doPasswordsMatch = password == confirmPassword + val showConfirmField = isPasswordValid + val canProceed = isPasswordValid && doPasswordsMatch + + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = w(20f), + end = w(20f), + top = h(52f), + bottom = h(48f + 24f) + ), + horizontalAlignment = Alignment.Start + ) { + + StepIndicator( + currentStep = 1, + totalSteps = 3, + label = "계정 정보" + ) + + Spacer(Modifier.height(h(32f))) + + Text( + text = "사용하실 비밀번호를\n 입력해주세요", + fontSize = 22.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Bold + ) + + Spacer(Modifier.height(h(32f))) + + // 비밀번호 입력 : 눈 가리개 있는 것으로 교체 + PasswordLoginTextField( + value = password, + onValueChange = { + password = it + signUpViewModel.password = it + }, + hint = "비밀번호를 입력해주세요." + ) + + Spacer(Modifier.height(h(10f))) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = w(12f)), + verticalArrangement = Arrangement.spacedBy(h(8f)) + ) { + PasswordRuleItem( + text = "영문, 숫자, 특수기호 조합", + satisfied = isPasswordComplex + ) + PasswordRuleItem( + text = "8~20자", + satisfied = isPasswordLengthValid + ) + } + + if (showConfirmField) { + Spacer(Modifier.height(h(20f))) + + // 비밀번호 확인 눈 가리개 있는 거로 교체 + PasswordLoginTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + hint = "비밀번호를 확인해주세요." + ) + + if (confirmPassword.isNotEmpty() && !doPasswordsMatch) { + Text( + text = "비밀번호가 일치하지 않습니다. 다시 입력해주세요.", + fontSize = 13.sp, + fontFamily = paperlogyFamily, + color = Color(0xFFFF5E5E), + modifier = Modifier.padding( + start = w(8f), + top = h(4f) + ) + ) + } + } + } + + BottomGradientButton( + text = "다음", + enabled = canProceed, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = { + signUpViewModel.password = password + navigator.navigate("sign_up_nickname") + }, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} + + +@Composable +fun SignUpPasswordScreenContent( + password: String, + confirmPassword: String, + onPasswordChange: (String) -> Unit, + onConfirmPasswordChange: (String) -> Unit, + canProceed: Boolean, + isPasswordComplex: Boolean, + isPasswordLengthValid: Boolean, + doPasswordsMatch: Boolean, + bottomPadding: Dp, + onNext: () -> Unit +) { + //디자인 모듈 불러오기. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + + Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = w(20f), + end = w(20f), + top = h(52f), + bottom = h(72f) + ) + ) { + StepIndicator( + currentStep = 1, + totalSteps = 3, + label = "계정 정보" + ) + + Spacer(Modifier.height(h(36f))) + + Text( + text = "사용하실 비밀번호를\n 입력해주세요", + fontSize = 22.sp, + fontFamily = paperlogyFamily, + fontWeight = FontWeight.Bold, + color = colorTheme.black + ) + + Spacer(Modifier.height(h(32f))) + + PasswordLoginTextField( + value = password, + onValueChange = onPasswordChange, + hint = "비밀번호를 입력해주세요." + ) + + Spacer(Modifier.height(h(12f))) + + // 조건 표시(체크박스 활성화/비활성화) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = w(12f)), + horizontalArrangement = Arrangement.spacedBy(w(24f)), + verticalAlignment = Alignment.CenterVertically + ) { + PasswordRuleItem( + text = "영문, 숫자, 특수기호 조합", + satisfied = isPasswordComplex + ) + + PasswordRuleItem( + text = "8~20자", + satisfied = isPasswordLengthValid + ) + } + + if (password.length >= 8) { + Spacer(Modifier.height(h(20f))) + PasswordLoginTextField( + value = confirmPassword, + onValueChange = onConfirmPasswordChange, + hint = "비밀번호를 확인해주세요." + ) + + if (confirmPassword.isNotEmpty() && !doPasswordsMatch) { + Text( + text = "비밀번호가 일치하지 않습니다.", + fontSize = 13.sp, + fontFamily = paperlogyFamily, + color = Color(0xFFFF5E5E), + modifier = Modifier.padding(start = w(8f), top = h(4f)) + ) + } + } + } + + // 하단 버튼 + BottomGradientButton( + text = "다음", + enabled = canProceed, + activeGradient = colorTheme.maincolor, + inactiveGradient = colorTheme.inactiveColor, + onClick = onNext, + modifier = Modifier.align(Alignment.BottomCenter) + ) + + } +} + + + + +@Preview(showBackground = true) +@Composable +fun SignUpPasswordScreenContentPreview() { + SignUpPasswordScreenContent( + password = "Test@1234", + confirmPassword = "Test@1234", + onPasswordChange = {}, + onConfirmPasswordChange = {}, + canProceed = true, + isPasswordComplex = true, + isPasswordLengthValid = true, + doPasswordsMatch = true, + bottomPadding = 16.dp, + onNext = {} + ) +} diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpScreen.kt similarity index 73% rename from feature/login/src/main/java/com/example/login/auth/SignUpScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/SignUpScreen.kt index 99a8c6f7..4f5de396 100644 --- a/feature/login/src/main/java/com/example/login/auth/SignUpScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpScreen.kt @@ -1,4 +1,4 @@ -package com.example.login.auth +package com.example.login.ui.screen import androidx.compose.foundation.background @@ -21,18 +21,30 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens + //어차피.. 수정되니까.. 리펙X @Preview(showBackground = true) @Composable fun PasswordResetScreen() { + + // 1. 디자인 시스템 토큰 및 반응형 유틸 가져오기 + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp, vertical = 80.dp), + .background(colorTheme.white) // 배경 시스템 화이트 적용 + .padding(horizontal = w(16f), vertical = h(80f)), verticalArrangement = Arrangement.SpaceBetween, // 상단/하단 나눠서 horizontalAlignment = Alignment.Start // 왼쪽 정렬 ) { + + Column( horizontalAlignment = Alignment.Start // 왼쪽 정렬 ) { @@ -40,48 +52,43 @@ fun PasswordResetScreen() { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.logo_whiteback), contentDescription = "Logo", - modifier = Modifier.size(width = 105.dp, height = 105.dp), + modifier = Modifier.size(width = w(105f), height = h(105f)), tint = Color.Unspecified ) - Spacer(modifier = Modifier.height(-8.dp)) + Spacer(modifier = Modifier.height(h(-8f))) // 타이틀 Text( "비밀번호 재설정", fontSize = 22.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - color = Color.Black, + color = colorTheme.black, textAlign = TextAlign.Start ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(h(4f))) // 서브텍스트 Text( "걱정 마세요! 이메일 주소를 입력해 주시면,\n임시 비밀번호를 보내드릴게요!", fontSize = 13.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Normal, - color = Color(0xFF757575), + color = colorTheme.gray[600]!!, textAlign = TextAlign.Start ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(h(32f))) // 이메일 입력 필드 Box( modifier = Modifier .fillMaxWidth() - .height(56.dp) + .height(h(56f)) .background( - brush = Brush.horizontalGradient( - colors = listOf( - Color(0xFF2C6FFF), - Color(0xFFC800FF) - ) - ), + brush = colorTheme.maincolor, shape = RoundedCornerShape(16.dp) ) .padding(1.dp) @@ -93,9 +100,9 @@ fun PasswordResetScreen() { Text( "이메일 주소를 입력해주세요.", fontSize = 13.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Normal, - color = Color(0xFF757575) + color = colorTheme.gray[400]!! ) }, singleLine = true, @@ -117,15 +124,9 @@ fun PasswordResetScreen() { Box( modifier = Modifier .fillMaxWidth() - .height(48.dp) + .height(h(48f)) .background( - brush = Brush.horizontalGradient( - colors = listOf( - Color(0xFF9BCBFF), // 연파랑 - Color(0xFFF4AFFF) // 연핑크 - //**이후 제대로 입력하면 컬러를 Color(0xFF2C6FFF),Color(0xFFC800FF)로 변경을 해야함.** - ) - ), + brush = colorTheme.inactiveColor, shape = RoundedCornerShape(18.dp) ), contentAlignment = Alignment.Center @@ -133,7 +134,7 @@ fun PasswordResetScreen() { Text( text = "임시 비밀번호 받기", color = Color.White, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontSize = 16.sp, fontWeight = FontWeight.Bold ) diff --git a/feature/login/src/main/java/com/example/login/auth/WelcomeScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/WelcomeScreen.kt similarity index 67% rename from feature/login/src/main/java/com/example/login/auth/WelcomeScreen.kt rename to feature/login/src/main/java/com/example/login/ui/screen/WelcomeScreen.kt index d29d861b..5e424131 100644 --- a/feature/login/src/main/java/com/example/login/auth/WelcomeScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/WelcomeScreen.kt @@ -1,4 +1,4 @@ -package com.example.login.auth +package com.example.login.ui.screen import android.util.Log import androidx.activity.compose.BackHandler @@ -21,27 +21,34 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import com.example.design.theme.LocalColorTheme +import com.example.design.util.rememberFigmaDimens +import com.example.login.viewmodel.SignUpViewModel @Composable fun WelcomeScreen( navigator: NavHostController, - signUpViewModel: SignUpViewModel? = null // null 허용, 프리뷰 확인용 - //signUpViewModel: SignUpViewModel = hiltViewModel() + signUpViewModel: SignUpViewModel? = null ) { + //디자인 모듈 가져오기. + val colorTheme = LocalColorTheme.current + val (w, h) = rememberFigmaDimens() + val paperlogyFamily = Paperlogy.font + val density = LocalDensity.current + val configuration = LocalConfiguration.current // 뒤로가기 막기 BackHandler { @@ -77,6 +84,17 @@ fun WelcomeScreen( } } + // 하단 동적 패딩 계산 로직 -> 기존 회원가입 바텀 그라데이션 버튼과 동일하게 작동함. + val imeBottom = WindowInsets.ime.getBottom(density) + val navBottom = WindowInsets.navigationBars.getBottom(density) + val screenHeight = configuration.screenHeightDp.dp + + val bottomPadding = when { + imeBottom > 0 -> 20.dp + navBottom > 0 -> screenHeight * (16f / 917f) + else -> screenHeight * (24f / 917f) + } + Box( modifier = Modifier .fillMaxSize() @@ -91,49 +109,53 @@ fun WelcomeScreen( ) { // 중앙 콘텐츠 (Column) Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally ) { + + // 로고 위치 (394/917) + Spacer(modifier = Modifier.height(h(394f))) Image( painter = painterResource(id = R.drawable.img_logo_white), contentDescription = "Logo", - modifier = Modifier.size(80.dp), + Modifier + .offset(x = w(160f) - (configuration.screenWidthDp.dp / 2) + w(46f)) // 시작 너비 보정 + .width(w(92f)) + .height(h(65f)), contentScale = ContentScale.Fit ) - Spacer(modifier = Modifier.height(0.dp)) + Spacer(modifier = Modifier.height(h(20f))) Text( text = "링큐에 오신 걸 환영해요!", - color = Color.White, + color = colorTheme.white, fontSize = 22.sp, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy + fontFamily = paperlogyFamily, + modifier = Modifier.fillMaxWidth().padding(start = w(99f)), + textAlign = TextAlign.Start ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(h(16f))) Text( text = "당신을 위한 링크, 링큐가 기억하고 연결해줄게요!", - color = Color.White, + color = colorTheme.white, fontSize = 16.sp, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth().padding(start = w(54f)) ) } // 버튼을 Box의 직접 자식으로 두고, 하단 정렬 Box( modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter) // 항상 하단 고정 - .imePadding() // 키보드 올라오면 자동 위로 - .navigationBarsPadding() // 내비/제스처 바 안전 영역 - .padding(start = 20.dp, end = 20.dp, bottom = 16.dp) - .height(48.dp) + .align(Alignment.BottomCenter) + .padding(start = w(20f), end = w(20f), bottom = bottomPadding) + .height(h(50f)) .background( Color.White, shape = RoundedCornerShape(18.dp) @@ -151,12 +173,8 @@ fun WelcomeScreen( text = "회원가입 완료하기", fontSize = 16.sp, fontWeight = FontWeight.Bold, - style = TextStyle( - brush = Brush.horizontalGradient( - colors = listOf(Color(0xFF2C6FFF), Color(0xFFC800FF)) - ) - ), - fontFamily = Paperlogy + style = TextStyle(brush = colorTheme.maincolor), + fontFamily = paperlogyFamily ) } diff --git a/feature/login/src/main/java/com/example/login/auth/MarketingTermsScreen.kt b/feature/login/src/main/java/com/example/login/ui/terms/MarketingTermsScreen.kt similarity index 92% rename from feature/login/src/main/java/com/example/login/auth/MarketingTermsScreen.kt rename to feature/login/src/main/java/com/example/login/ui/terms/MarketingTermsScreen.kt index 83603a06..ab83a25f 100644 --- a/feature/login/src/main/java/com/example/login/auth/MarketingTermsScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/terms/MarketingTermsScreen.kt @@ -1,15 +1,12 @@ -package com.example.login.auth +package com.example.login.ui.terms import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,11 +24,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy -/* ───────────────────────────── - 공통 풋터 버튼 (양식 통일용) - ───────────────────────────── */ +//이거는 ui 자체가 바뀔 예정. 리펙하지 않음. private val FOOTER_HEIGHT = 50.dp private val FOOTER_BOTTOM = 0.dp // AgreeFooterButton 내부 .padding(..., bottom = 30.dp) private val EXTRA_GAP = 0.dp // 버튼 바로 위에 살짝 여유 @@ -45,6 +40,10 @@ private fun AgreeFooterButton( text: String = "약관에 동의합니다", applyNavPadding: Boolean = false, ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + Box( modifier = modifier .fillMaxWidth() @@ -68,7 +67,7 @@ private fun AgreeFooterButton( text = text, color = if (enabled) Color.White else Color.Gray, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } @@ -84,6 +83,10 @@ fun MarketingTermsScreenComposable( onAgreeClicked: () -> Unit, onBackClicked: () -> Unit ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + val scrollState = rememberScrollState() val isAtBottom by remember { derivedStateOf { scrollState.value >= scrollState.maxValue } } @@ -95,7 +98,7 @@ fun MarketingTermsScreenComposable( text = "마케팅 수신 동의", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, modifier = Modifier.padding(horizontal = 20.dp) ) }, @@ -146,7 +149,7 @@ fun MarketingTermsScreenComposable( text = "마케팅 수신 동의서", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, color = Color.Black ) @@ -164,7 +167,7 @@ fun MarketingTermsScreenComposable( """.trimIndent(), fontSize = 14.sp, lineHeight = 22.sp, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) Spacer(Modifier.height(12.dp)) @@ -197,7 +200,7 @@ fun MarketingTermsScreenComposable( """.trimIndent(), fontSize = 14.sp, lineHeight = 22.sp, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } diff --git a/feature/login/src/main/java/com/example/login/auth/PrivacyTermsScreen.kt b/feature/login/src/main/java/com/example/login/ui/terms/PrivacyTermsScreen.kt similarity index 92% rename from feature/login/src/main/java/com/example/login/auth/PrivacyTermsScreen.kt rename to feature/login/src/main/java/com/example/login/ui/terms/PrivacyTermsScreen.kt index adef7197..b247cd7c 100644 --- a/feature/login/src/main/java/com/example/login/auth/PrivacyTermsScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/terms/PrivacyTermsScreen.kt @@ -1,11 +1,9 @@ -package com.example.login.auth +package com.example.login.ui.terms import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -19,15 +17,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy import androidx.compose.ui.draw.clip import androidx.compose.runtime.remember import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -/* ───────────────────────────── - 공통 풋터 버튼 (양식 통일용) - ───────────────────────────── */ +//여기 ui가 바뀔 예정 리펙토링 진행하지 않음. 디자인, 약관 확정시 수정(1월 말) private val FOOTER_HEIGHT = 50.dp private val FOOTER_BOTTOM = 0.dp private val EXTRA_GAP = 0.dp @@ -40,6 +36,10 @@ private fun AgreeFooterButton( text: String = "약관에 동의합니다", applyNavPadding: Boolean = true, ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + Box( modifier = modifier .fillMaxWidth() @@ -63,7 +63,7 @@ private fun AgreeFooterButton( text = text, color = if (enabled) Color.White else Color.Gray, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } @@ -77,6 +77,10 @@ fun PrivacyTermsScreen( onAgreeClicked: () -> Unit, onBackClicked: () -> Unit ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + val scrollState = rememberScrollState() val isAtBottom by remember { derivedStateOf { scrollState.value >= scrollState.maxValue } } @@ -88,7 +92,7 @@ fun PrivacyTermsScreen( text = "개인정보 처리방침", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, modifier = Modifier.padding(horizontal = 20.dp) ) }, @@ -137,7 +141,7 @@ fun PrivacyTermsScreen( text = "개인정보 수집 및 이용 동의서", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, color = Color.Black ) @@ -157,7 +161,7 @@ fun PrivacyTermsScreen( """.trimIndent(), fontSize = 14.sp, lineHeight = 22.sp, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) Spacer(Modifier.height(12.dp)) // ⬅️ 본문 단락 간격 동일 @@ -201,7 +205,7 @@ fun PrivacyTermsScreen( """.trimIndent(), fontSize = 14.sp, lineHeight = 22.sp, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } diff --git a/feature/login/src/main/java/com/example/login/auth/ServiceTermsScreen.kt b/feature/login/src/main/java/com/example/login/ui/terms/ServiceTermsScreen.kt similarity index 92% rename from feature/login/src/main/java/com/example/login/auth/ServiceTermsScreen.kt rename to feature/login/src/main/java/com/example/login/ui/terms/ServiceTermsScreen.kt index 79c73e43..7529cb7f 100644 --- a/feature/login/src/main/java/com/example/login/auth/ServiceTermsScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/terms/ServiceTermsScreen.kt @@ -1,16 +1,13 @@ -package com.example.login.auth +package com.example.login.ui.terms import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -19,16 +16,13 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.login.R -import com.example.login.Paperlogy +import com.example.design.theme.font.Paperlogy -/* ───────────────────────────── - 공통 풋터 버튼 (양식 통일용) - ───────────────────────────── */ +//ui 수정 예정. 약관, 디자인 확정시 수정함. private val FOOTER_HEIGHT = 50.dp private val FOOTER_BOTTOM = 0.dp private val EXTRA_GAP = 0.dp @@ -41,6 +35,9 @@ private fun AgreeFooterButton( text: String = "약관에 동의합니다", applyNavPadding: Boolean = true, ) { + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + Box( modifier = modifier .fillMaxWidth() @@ -64,7 +61,7 @@ private fun AgreeFooterButton( text = text, color = if (enabled) Color.White else Color.Gray, fontWeight = FontWeight.Bold, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } @@ -78,6 +75,10 @@ fun ServiceTermsScreen( onAgreeClicked: () -> Unit, onBackClicked: () -> Unit ) { + + // 2. 디자인 모듈의 폰트 패밀리 가져오기 + val paperlogyFamily = Paperlogy.font + val scrollState = rememberScrollState() val isAtBottom by remember { derivedStateOf { scrollState.value >= scrollState.maxValue } } @@ -126,7 +127,7 @@ fun ServiceTermsScreen( text = "서비스 이용약관", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, modifier = Modifier.padding(horizontal = 20.dp) ) }, @@ -175,7 +176,7 @@ fun ServiceTermsScreen( text = "앱 서비스 이용약관", fontSize = 16.sp, fontWeight = FontWeight.Medium, - fontFamily = Paperlogy, + fontFamily = paperlogyFamily, color = Color.Black ) @@ -186,7 +187,7 @@ fun ServiceTermsScreen( text = serviceTermsBody, fontSize = 14.sp, lineHeight = 22.sp, - fontFamily = Paperlogy + fontFamily = paperlogyFamily ) } } diff --git a/feature/login/src/main/java/com/example/login/auth/TermsDetailScreen.kt b/feature/login/src/main/java/com/example/login/ui/terms/TermsDetailScreen.kt similarity index 93% rename from feature/login/src/main/java/com/example/login/auth/TermsDetailScreen.kt rename to feature/login/src/main/java/com/example/login/ui/terms/TermsDetailScreen.kt index bc922c93..3b6dcb03 100644 --- a/feature/login/src/main/java/com/example/login/auth/TermsDetailScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/terms/TermsDetailScreen.kt @@ -1,7 +1,5 @@ -package com.example.login.auth +package com.example.login.ui.terms -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -9,10 +7,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.sp diff --git a/feature/login/src/main/java/com/example/login/auth/EmailAuthViewModel.kt b/feature/login/src/main/java/com/example/login/viewmodel/EmailAuthViewModel.kt similarity index 92% rename from feature/login/src/main/java/com/example/login/auth/EmailAuthViewModel.kt rename to feature/login/src/main/java/com/example/login/viewmodel/EmailAuthViewModel.kt index 31463774..b5de95f5 100644 --- a/feature/login/src/main/java/com/example/login/auth/EmailAuthViewModel.kt +++ b/feature/login/src/main/java/com/example/login/viewmodel/EmailAuthViewModel.kt @@ -1,27 +1,24 @@ -package com.example.login.auth +package com.example.login.viewmodel -import android.content.Context +import android.util.Log +import android.util.Patterns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.core.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import android.util.Log -import com.example.core.repository.UserRepository -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.SharingStarted -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlin.random.Random -import retrofit2.HttpException import okhttp3.ResponseBody import org.json.JSONObject - -// 이메일 인증과 관련된 로직을 담당하는 ViewModel +import retrofit2.HttpException +import javax.inject.Inject +import kotlin.random.Random +//여기 api 전면 수정 예정. 실제 api 연동은 1월 말~ 2월 초 @HiltViewModel class EmailAuthViewModel @Inject constructor( private val userRepository: UserRepository @@ -56,7 +53,7 @@ class EmailAuthViewModel @Inject constructor( // 6자리 랜덤 코드 생성 함수 private fun generateRandomSixDigitCode(): String { - return Random.nextInt(0, 1_000_000) + return Random.Default.nextInt(0, 1_000_000) .toString() .padStart(6, '0') } @@ -74,7 +71,7 @@ class EmailAuthViewModel @Inject constructor( fun sendEmailCode(email: String) { Log.d("EmailAuthVM", " sendEmailCode() called. email=$email") viewModelScope.launch { - if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { Log.w("EmailAuthVM", "Invalid email format: $email") _sendCodeResult.value = "잘못된 이메일 형식" return@launch @@ -122,7 +119,7 @@ class EmailAuthViewModel @Inject constructor( _isVerifySuccess.value = ok if (ok) { // 네비게이션 트리거 후 바로 false로 reset (재진입 자동 네비 방지) - kotlinx.coroutines.delay(200) + delay(200) _isVerifySuccess.value = false } } catch (e: Exception) { @@ -133,5 +130,4 @@ class EmailAuthViewModel @Inject constructor( } } } -} - +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/auth/LoginViewModel.kt b/feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt similarity index 90% rename from feature/login/src/main/java/com/example/login/auth/LoginViewModel.kt rename to feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt index 86e5232e..4be0f22a 100644 --- a/feature/login/src/main/java/com/example/login/auth/LoginViewModel.kt +++ b/feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt @@ -1,31 +1,26 @@ -package com.example.login.auth - - +package com.example.login.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.core.model.LoginResult import com.example.core.repository.UserRepository -//import com.example.data.preference.AuthPreference +import com.example.core.session.SessionStore +import com.example.data.preference.AuthPreference import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -import android.util.Log import retrofit2.HttpException import java.util.concurrent.atomic.AtomicBoolean - -//로그인 뷰모델 : 로그인 로직 담당. 레포지토리를 통해 로그인 api 수학. -//로그인 성공시 사용자 세션 및 userId 전달. -//예외 발생 시 UI 에러태그 전달. +import javax.inject.Inject @HiltViewModel open class LoginViewModel @Inject constructor( private val repo: UserRepository, - private val sessionStore: com.example.core.session.SessionStore, - private val authPreference: com.example.data.preference.AuthPreference, + private val sessionStore: SessionStore, + private val authPreference: AuthPreference, ) : ViewModel() { // UI가 사용할 단일 상태 @@ -122,5 +117,4 @@ open class LoginViewModel @Inject constructor( } } } -} - +} \ No newline at end of file diff --git a/feature/login/src/main/java/com/example/login/auth/ResetPasswordViewModel.kt b/feature/login/src/main/java/com/example/login/viewmodel/ResetPasswordViewModel.kt similarity index 92% rename from feature/login/src/main/java/com/example/login/auth/ResetPasswordViewModel.kt rename to feature/login/src/main/java/com/example/login/viewmodel/ResetPasswordViewModel.kt index 12c9f19b..18cdadd3 100644 --- a/feature/login/src/main/java/com/example/login/auth/ResetPasswordViewModel.kt +++ b/feature/login/src/main/java/com/example/login/viewmodel/ResetPasswordViewModel.kt @@ -1,4 +1,4 @@ -package com.example.login.auth +package com.example.login.viewmodel //유저 비밀번호 재설정 기능 수정으로 리펙X @@ -11,6 +11,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject + +//여기 api 전면 수정 예정. 실제 api 연동은 1월 말~ 2월 초 data class ResetPwUiState( val loading: Boolean = false, val success: Boolean = false, diff --git a/feature/login/src/main/java/com/example/login/auth/SignUpViewModel.kt b/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt similarity index 97% rename from feature/login/src/main/java/com/example/login/auth/SignUpViewModel.kt rename to feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt index e51c4f90..4fa859aa 100644 --- a/feature/login/src/main/java/com/example/login/auth/SignUpViewModel.kt +++ b/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt @@ -1,19 +1,17 @@ -package com.example.login.auth - +package com.example.login.viewmodel import android.util.Log -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.core.model.UserInfo import com.example.core.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue //by 사용을 위해 /** * 회원가입 흐름을 관리하는 ViewModel @@ -147,5 +145,4 @@ class SignUpViewModel @Inject constructor( -} - +} \ No newline at end of file diff --git a/feature/login/src/main/res/drawable/ic_login_check.png b/feature/login/src/main/res/drawable/ic_login_check.png new file mode 100644 index 0000000000000000000000000000000000000000..a82f272b082b6213515735a216a6c458ea3aaaa1 GIT binary patch literal 410 zcmV;L0cHM)P)@~0drDELIAGL9O(c600d`2O+f$vv5yP`>Y#DZI*5cyh=fW&LL}V6kPjx4@(N|?ncZWL!bUUFX7Qr<}_=pzJB4>W*a)2<)mQH9DBk05+o7?UP!CCa+nAOqkO+OtZ(E_i4 z;0=;!fk!}afFxSr{_lcMNMd3vAozkLTA&LEno};G3ulj@Vr(v4W;@cRh8_iATs1kx z4%TlBaK{cH>BwbT^}jGwj;A*w_L9nmNAP^|ejVfdQA zCMmC$l8kvUx%7}AQgjv*bfHa8YC~>hgFlwc%62uyKA(A?TMOyX+yDRo07*qoM6N<$ Ef^GZx^prwfgF}}M_)$E)e-c?47?@{zx;TbZFutAZ?|0ilr2YPEi_j%yf+`xX zx(b+tI44Ai<_VaECOB6!Jz!~(KEOFE&+7{BorVo-9K5QSE`_!($hxsbw9mFSSxWm! zfAaZc#*@EQo*c2zvAdUlY~B%lj(&%|6*EhX^}84RwQ%_E%TvG=9C*A0r)nyre=-je&R9%glUTKhsr#>@hia za<)zlo-b0#HmjhB&+2P;dHB)5<8E9w_y6p4|KEEm!oAEQa&FM^gBwH-Uf__fEQtzY zQ!I*+(YP>W-9~;Vj-$W+KDzj2`MuEB3|Qh+oGX_=A@#Z4>`T?b0fL&!JpXzwUWVlR?+C&&Ox#&+*xMX7UE6*nm`lM-%p* zzVo(Z9*d~P)#)=fa5Zz4C~oC)5;~;C!*c1@T{Di(fCcwv>3Y??T`1wN*}2{}@0hOE zEH|~ZsZTE|mUpfDDCV%CDQshLn$S%4gIq2#229U^0+(jX?o`rsQDWNJ;h?58p_3y; ziD@QPnKS%fZg<$=*4P#;u*Tr7?UeO@**MBFeykJN$k`;z!7H4&(jof9T(9&?>IxAL z9d|9`n)=|{S1At9%rzm)XC16u&|kpjq~vqkW-~9#zKVx3Cw?Xv`P7^>6Uaz@}Mj2bSZnn!f^IA1h`CVbZNvLe literal 0 HcmV?d00001 diff --git a/feature/login/src/main/res/drawable/img_recent_login.png b/feature/login/src/main/res/drawable/img_recent_login.png new file mode 100644 index 0000000000000000000000000000000000000000..a01d870e8e50a5bd4e84045eb93d5790289c13ba GIT binary patch literal 4827 zcmX9?byyS58@~}!0;6%%C=o#k5u}lFG@_tG>6Q{{B!(yQ8*#?Rt# zl!oCU0_hKcw9$VBQl$Q>jG~12TinnFRd{|PMWAuJZgL%fnpCDE2U-Bw9~$Uhza0Wu znKJR>{~OW0ae74z+YqC@bkA(Iyls#0a7d73BrZ>DE#&y3y<2#w0P?(<&GI8HftSle z?2s_TG9L>y(Hnw=m#EllR8+-#~Ven@+D+vM!h*ptgTri?yg;eGCXq@Hu}#gx(4@-kJPYyQhi2L$=)WAIS!4$Q5( z!49^zkXE1Dj99vhakdi_GJ^1a6K76z9Xi2a$x^L89eo#Pf&GJ4wBM3rpQwkyrRsO- zg+TJsb0;e=%NqNRayDu6r<^&N>o7)=2{=bD2~Kzp zw+oP)B{sJrzLlOmgD4@mSF*!^yjfgJQl+VN$%IJ$khqRKJKWkkyxEo7DQi{|G%aK| zSDJdE6@ta7Ed=4yvO^|vt13fhP@O-^zhu}Pj=yO;qbOEn0fW=f0gO7rg8RSD_BGb@ z#bONNIn^H`mt3W4#Rxz#)yFtrxul-XNrNbJJmpbU=!V$qR<-!@tv03$4riz7+SKY; zZvF_uk&qCOq+gw-Km0YX!8ey5q0L^dRjLBu>?;nHMPlV?G#8=eEFeB9t(G^q-%8X> z*HklIGTRaL@2u38(p*n0b80#OB97H%U1~C}#nP+QCoPrw+48ivV4VbEDgTjTXG;J! zvgXjQL~C*eD}M<=1%WJWGpBZSQ`kx}vJYZm%{8xP?@b4c(`^0ovf`$%UelK&YZ*3r zt9cKs0n}4oWP*RWuQ6oqS$8Y|=|$hg9U0Sjsh+L_)a?XGT@e;&CpEB;jD;r1YSFU- zL_@reSOKWXi}cQ<282oh5oSJ-R51)aH#^>PV(=>fW4{HBunnB#vt;y=R=~6UJ{t&d zj+bn#o+85p7wdYy^pZ7yhWChE2R#=?etm6omP(s<%F>D#jBk{i+oA$J=S4ftFV>~w zO4h#9oIi^;TI$15q10r5B&cqMq*wly;O!)|$9cw8E%U+voUXpirz)i-W^KeY>JnGQugnRwOp}#fUOj-m@>`<^@a`_Y zUYMMgq34ISPd~Z9pFE#H5wp~QHX(2N?=M-9vh;vrcOc-IB95^MN9C}{1?;uB9$lv`Skj>EEy_wbziQ_&=LF)|X959QO|DHdQxQ~u zm@MUdC~h{NCT$ZO*H_2h1P7SXnNDb5^TgU^>PAO^rO-6hhoxc!aci)jBf^Inb`Ijyk@(2MsF+TtJrK+#)iqEn9K2^)bvu!zqMJD!^q^Ww1~~E zs^i^uvjz0UFtq)x3BCrr`V?fYesqhmsgP&3p<$aQ-WGm?0bNw&Y4Zc~LeU53`EYHy z^-_jh$C=|$@5#u1mfd!*4zud<@;~ixaem|us}pVZztvixg=jn_T@79LJq>dR;UDYO z_Eqr;4F6CvMUF+CaR5a~6|e$7ep z9WUB}#{(mb`f9x6#i$bkyty?na( zpiyr+f%$uf<9A#F1M7v;e`k`PM7&qq+c*@B@H!qSOpDd%ddK=_{?oma?gy~b4~j0Y zMKXzslFU7-zU{*E)*U+!M!X6b@}ER(V(KSX16klVTzAC>r)wt(Lht*f1hzL3HT#e$vd@}zE9qz1>po0#d^^Te8{ zybxI*l>59U;&Y2nT)BQj!||>KR$x^4{q7GPUAxug){K7~klcZaYyQL3lCLM2^nks5 z;z;$5+Bw4cy=$`r?}Y015A1d$BPTL%bhcKwOB^Cm3dm3vIH!%+QiL!2V_h)=i;s-i`(R6|iTGq*Ku zZ>@KI^+fp@2LH)&^7V1Z9Bk^;P&UgCHt^~E!R#zs@^$AovwovH` zX&(5BD{oL2Y*livS{a%fhCNS=pxMbeGH=hF>{G~<>-v87 zZ$-2%+ZWS(W!e!n-3>T3;e0Qn@Lto!3>ilDI|EYV(#+dLqEZaTRzhp8xGp%r@B-?> zafPE4G;Zkcty=E8UjHi1?CF`5->Q-hJQ~!!w!Yk?)ihh5(bjf#El;_2=^|S4=VCq{ z&M;l_BS}hfT$VLcw?jfZV}D)8Wa!y6S^9Ik>R~2ZvwpU0iHbrgJh> zaWfMWPsGl}4Xl-!(r=it542Tw+Aha@Zr=&p%d&~@JE|~A3;iIQ@Y)I%L+&MXjCfZE z3bw5VT4E%hAM70LKF;n!zlrS@x#y_s6LlIn0+Wca89_YTk7#c?F&vqo6K2iPetLiR z+zqIdK*!3+$Enr2x>7Pi@TB|3GZ=x;Kt;Lsc;C?QAY0B)Z7tiuc1n7=FMSwUCBBz6p2%v;C( zyZ%-+b#>-(wVQSIoo|z&I^5-Un@^t7u@Ow;vTw`kw$>4)WOi>ae0H}iOl8Kfnrx%D z$Bu?`e0ye3!INZuGi$xono<)J$9&7M%eiqV>7UT1O}-zoVV#AkbSoS|R18EN zTK4o+@4zdvwo4@r?;W}$yR&+6kwNsSg7d^`{Z?Z8TI;1dQHM?Dw?qT)mcEK6qWvuI zHCqXX1bwY1aw~?~K}SNhF=mlZI=;vJ+g+G$JDg6E3Ys!eq7pkl4u)MrA6oU6ZX;hu zE>8)ayv4pQSO_1bv$RSLNj8*-e}Q4Tkyu;M>_6OeBDmCvJU%&8;_?=XTOVbzJkE0~ z3Jrq7*$8w`H2evli_SB%beJ+!Gb8e{3I?MJ_gb~s6?p06!Uw=WNkQM2Y_x`FbKufY zTE6nt$W6bIAXX#0ljC=v4fx1o4=Tx!SqR(&b{j5ivV`QCl~Z0RT7KsKA25qv7-ub0?>l_23wpkR){c6a3>b34j^i=RP zDLW0Bu2t?4W-tVK7XaQICoJpYHwfsO?|nzsf>8m8QOE24cg9mz?T7KGd`3q< z1<&ppn4$rI^8^TAF8)|gO3B6lq6H{F-oK-Y1Ak#pj0cY22RI13l~E)549YaHOC@@S z(&vrOO+9N2->+4Ag`LoFzbpOE3Ys{AZ)};KaKie71b6qKdLH)XV!SnagV?U|=@K?B zi}f~h^GL=j^{;o)18ZtDNhSVWx}1wrQ<`;~S6+)waE(n+_clT;M{@Rd6AZB;US|>@ z_jl9U-Ra^zhL(lCKdG_2IQwV>t3aRBQz;!K{(+g}&MUiZpPTfTCw~o!_|8O{5ULP4 z*9%yk5?LBK;Fk{1NW?gI4Gg3;Iymu9}3HYzc@?cIxB z4TAb-F^<=dT;Ev=*)6Sm-+vhr;`bw)laMA^*g+G77bcC@t$k(9K5G)*WF z7zu;-hzSV(^dCF!>$@cyaeo|-OnscmvVpxU9pEjIu5cXT$SWn1BK^&vuDCjTwF9@E z(3U899iTKOojlY-i?^)oS2LO<0N8dRANvNk>v*P zx5AZ+T&Duj!qR!!3(E`uOg;Fd8a9&OI2i!}AFBhK1zNE`&YbVh#qj&+;d1|Mp1&1^kjp!YV6KGxj14V8mMq z)jB_(PmYXI{>=$3hk^^Zw!}^y=29(AOM1Zh>mx~rSvd~G5QRmQ5 z4*Y^K_cmWc07;sE6+TdO?Yl6=Hq#CmO$8$2@QNzd0no+H}0I2S#?wyFao z0kqSlCi#lp0Xb1oRb}oiDYIp%A4~wAs`%AArS;t^FBjskC~q$niXT?yx2mMgq5d(0 zZ@LKwP`N+r@PYl^Azi$kB>M|Y;L&2voW_1ylE)KJGx;L*OP{)v-D6V#&bfYA`8gD8 zY=0!!)AR`0$1%a`ho!{(A``n8Y`oW0t_*&+;pYbp?@x<<^8s~rNR_Kn&NThsY&NT_I%5?c;)9FFsB+O-3D{Ph? z1YpLBy|aA{XhayH16jzc+BCqz+n;dm&ao~J{5ceYs(-GacDo)@K+gscIX$0s^o0Je zju%(Etp+Jj(48C;b#;Siu}?b5Et(ZP$lT@lq=I$`$m{8|ai|dn#zs2UB{I)!{>t@n zQt8$>3V`Czzls*xIbEOamv+tIBSiY^j;;DW0N_}_Cu(b*tshdl7Y0|)HkshrPs?oT z4)R)6R?jj7fNdS`e&E`I^r>#RC0`_50gtVx{1Ww9{@flewhY`e@t~I~9aC2Ey9EHp z&+^Z0;wAlG#X5DwZ%6NqJ@}G&NsbcCf>@*Xq;;cGs*SoCusg`*8;r;cI|FbCoun;` zbj!FI&YXWbIElc=6B_qE+14oGc_1K_fKF*@D$m7_!qb+tEvevJ_2Uo8Uur)25h@H~on^F+T;JHTH8wa#f$wCacSIJr zXlvf1wK1vns@#_(WU1o8w;Nz+1v*%%k9Kw_ulV4}ter7%T90n}2M4F55mGK+J#DFA7QY@g?p@oOneNgWFF4279YVR&>a z*Z?imn*@!$55knk46a5wFhoB7*BWUQcLDJ5-84|<%)(MKQ`9?8Z#0ICNdu6Slx3AgHabWg49k+Wq9g_YFwisAt Date: Wed, 21 Jan 2026 22:11:46 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feature/#71=20:=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/linku_android/MainApp.kt | 37 ++----------------- .../ui/bottom_sheet/NoAnimBottomSheet.kt | 2 +- .../example/login/ui/item/StepIndicator.kt | 11 ++---- .../ui/screen/EmailVerificationScreen.kt | 4 +- .../login/ui/screen/SignUpNicknameScreen.kt | 16 +++++--- .../login/ui/screen/SignUpPasswordScreen.kt | 6 +-- 6 files changed, 23 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/example/linku_android/MainApp.kt b/app/src/main/java/com/example/linku_android/MainApp.kt index ea90c228..42443002 100644 --- a/app/src/main/java/com/example/linku_android/MainApp.kt +++ b/app/src/main/java/com/example/linku_android/MainApp.kt @@ -260,51 +260,20 @@ fun MainApp( ) { /* ① Login composable */ - composable(NavigationRoute.Login.route) { entry -> - val parentEntry = entry + composable(NavigationRoute.Login.route) { parentEntry -> val signUpVm: SignUpViewModel = hiltViewModel(parentEntry) - val showTermsSheet by parentEntry.savedStateHandle - .getStateFlow("show_terms_sheet", false) - .collectAsStateWithLifecycle() - - - // 이메일 인증에서 백버튼으로 갔을 때, 약관 페이지 나오는게 맞는지. - - // 이메일 인증에서 돌아오는지 확인 - var cameFromEmail by remember { mutableStateOf(false) } - - LaunchedEffect(navigator.currentBackStackEntry) { - if (parentEntry.savedStateHandle.get("from_email_verification") == true) { - cameFromEmail = true - parentEntry.savedStateHandle["show_terms_sheet"] = true - - kotlinx.coroutines.delay(120) - - cameFromEmail = false - parentEntry.savedStateHandle["from_email_verification"] = false - } - } - - // 약간의 지연 + 재렌더링 위해 빈 박스 만듬. - if (cameFromEmail) { - Box(Modifier.fillMaxSize()) {} - return@composable - } - - val skipAnimation = parentEntry.savedStateHandle .get("skip_login_animation") == true AnimatedLoginScreen( navigator = navigator, - skipAnimation = skipAnimation, // 백버튼시 애니메이션 스탑 플래그 전달 + skipAnimation = skipAnimation, onSignUpClick = { parentEntry.savedStateHandle["show_terms_sheet"] = true } ) - } /* ② Service Terms */ @@ -450,7 +419,7 @@ fun MainApp( composable("email_login") { - val parentEntry = remember { + val parentEntry = remember(navigator.currentBackStackEntry) { navigator.getBackStackEntry("auth_graph") } diff --git a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt index edf2d41e..8cef0bb7 100644 --- a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt +++ b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt @@ -23,7 +23,7 @@ fun NoAnimBottomSheet( onDismissRequest: () -> Unit, scrimColor: Color = Color.Black.copy(alpha = 0.12f), shape: Shape, - containerColor: Color? = null, // 기본 컨테이너 컬러를 디자인 모듈의 white로 변경 + containerColor: Color = Color.White, //null 대신 white로 재변경. content: @Composable ColumnScope.() -> Unit ) { if (!visible) return diff --git a/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt b/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt index d111971c..9fc7b750 100644 --- a/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt +++ b/feature/login/src/main/java/com/example/login/ui/item/StepIndicator.kt @@ -24,19 +24,16 @@ fun StepIndicator( totalSteps: Int, label: String, modifier: Modifier = Modifier, - activeColor: Color = Color(0xFFCB59EB), - inactiveColor: Color = Color(0xFFD6D6D6), - completedColor: Color = Color(0xFFE5ACF4) + ) { val colorTheme = LocalColorTheme.current val (w, h) = rememberFigmaDimens() val paperlogyFamily = Paperlogy.font - // 100% 일치하는 컬러만 토큰으로 매칭 - val finalActiveColor = colorTheme.purple[200] - val finalCompletedColor = colorTheme.purple[100] - val finalInactiveColor = inactiveColor + val activeColor = colorTheme.purple[200] + val completedColor = colorTheme.purple[100] + val inactiveColor = Color(0xFFD6D6D6) // 디자인 토큰 없으면 유지하는 컬러. Column( modifier = modifier, diff --git a/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt index 70927751..442314de 100644 --- a/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/EmailVerificationScreen.kt @@ -161,7 +161,7 @@ fun EmailVerificationScreen( /** * UI만 그리는 프레젠테이션 컴포저블입니다. * Preview에서 ViewModel 없이 안전하게 사용 가능. - * 여기 이메일 인증에서는 """ui"""만 당당합니다 + * 여기 이메일 인증에서는 """ui"""만 담당합니다 */ @Composable fun EmailVerificationScreenContent( @@ -198,7 +198,7 @@ fun EmailVerificationScreenContent( start = w(20f), end = w(20f), top = h(60f), - bottom = h(48f + 24f) + bottom = h(72f) ), horizontalAlignment = Alignment.Start ) { diff --git a/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt index d10476ba..0dc8a29d 100644 --- a/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt @@ -81,18 +81,22 @@ fun SignUpNicknameScreen( Spacer(Modifier.height(h(32f))) + //입력값 기준으로 즉시 판단, 삭제 시 불필요한 호출을 방지하도록 수정함. LoginTextField( value = nickname, - onValueChange = { - nickname = it - signUpViewModel.nickname = it - if (isNicknameValid) { + onValueChange = { input -> + nickname = input + signUpViewModel.nickname = input + + val isValid = + input.isNotBlank() && input.length <= 6 + + if (isValid) { signUpViewModel.checkNickname() } }, hint = "닉네임을 입력해주세요.", - modifier = Modifier.fillMaxWidth(), - + modifier = Modifier.fillMaxWidth() ) if (isNicknameAvailable == false) { diff --git a/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt index af11c856..0b9fb4b9 100644 --- a/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt @@ -160,11 +160,11 @@ fun SignUpPasswordScreenContent( confirmPassword: String, onPasswordChange: (String) -> Unit, onConfirmPasswordChange: (String) -> Unit, + showConfirmField: Boolean, canProceed: Boolean, isPasswordComplex: Boolean, isPasswordLengthValid: Boolean, doPasswordsMatch: Boolean, - bottomPadding: Dp, onNext: () -> Unit ) { //디자인 모듈 불러오기. @@ -229,7 +229,7 @@ fun SignUpPasswordScreenContent( ) } - if (password.length >= 8) { + if (showConfirmField) { Spacer(Modifier.height(h(20f))) PasswordLoginTextField( value = confirmPassword, @@ -273,11 +273,11 @@ fun SignUpPasswordScreenContentPreview() { confirmPassword = "Test@1234", onPasswordChange = {}, onConfirmPasswordChange = {}, + showConfirmField = true, canProceed = true, isPasswordComplex = true, isPasswordLengthValid = true, doPasswordsMatch = true, - bottomPadding = 16.dp, onNext = {} ) } From ba641d5258d4ead74d6e16acc55c530b6e8dc463 Mon Sep 17 00:00:00 2001 From: Chea-yunzi Date: Wed, 21 Jan 2026 22:59:50 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feature/#71=20:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/example/linku_android/MainApp.kt | 6 ++++++ .../com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt | 2 +- .../com/example/login/ui/screen/SignUpNicknameScreen.kt | 5 ++++- .../java/com/example/login/viewmodel/SignUpViewModel.kt | 7 +++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/example/linku_android/MainApp.kt b/app/src/main/java/com/example/linku_android/MainApp.kt index 42443002..8d96e885 100644 --- a/app/src/main/java/com/example/linku_android/MainApp.kt +++ b/app/src/main/java/com/example/linku_android/MainApp.kt @@ -267,6 +267,12 @@ fun MainApp( parentEntry.savedStateHandle .get("skip_login_animation") == true + // 읽은 직후 초기화 + LaunchedEffect(skipAnimation) { + if (skipAnimation) { + parentEntry.savedStateHandle["skip_login_animation"] = false + } + } AnimatedLoginScreen( navigator = navigator, skipAnimation = skipAnimation, diff --git a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt index 8cef0bb7..4ab3d64d 100644 --- a/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt +++ b/feature/login/src/main/java/com/example/login/ui/bottom_sheet/NoAnimBottomSheet.kt @@ -33,7 +33,7 @@ fun NoAnimBottomSheet( val (w, h) = rememberFigmaDimens() // 파라미터로 받은 컬러가 없으면 테마의 white 사용 - val finalContainerColor = containerColor ?: colorTheme.white + val finalContainerColor = containerColor Box( modifier = Modifier.fillMaxSize(), diff --git a/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt index 0dc8a29d..9cc1e989 100644 --- a/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt +++ b/feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt @@ -47,7 +47,7 @@ fun SignUpNicknameScreen( // 버튼 활성 조건 (EmailVerificationScreen의 isButtonEnabled와 동일한 느낌) val isButtonEnabled = isNicknameValid && - (isNicknameAvailable != false) && // false만 비활성, null(미확인) 허용 + (isNicknameAvailable == true) && // 중복확인 완료만 허용 !isLoading Box(modifier = Modifier.fillMaxSize()) { @@ -88,6 +88,9 @@ fun SignUpNicknameScreen( nickname = input signUpViewModel.nickname = input + // 입력 변경시 이전 결과 초기화 + signUpViewModel.resetNicknameAvailability() + val isValid = input.isNotBlank() && input.length <= 6 diff --git a/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt b/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt index 4fa859aa..3a8cf5a6 100644 --- a/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt +++ b/feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.kt @@ -42,6 +42,11 @@ class SignUpViewModel @Inject constructor( _agreeMarketing.value = v } + //닉네임 아직 검증되지 않음을 확인. + fun resetNicknameAvailability() { + _isNicknameAvailable.value = null + _nicknameMessage.value = null + } // 회원가입 전체 데이터 @@ -143,6 +148,4 @@ class SignUpViewModel @Inject constructor( } } - - } \ No newline at end of file