Skip to content
Merged
18 changes: 18 additions & 0 deletions app/src/main/java/com/sopt/clody/presentation/di/LanguageModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sopt.clody.presentation.di

import com.sopt.clody.presentation.utils.language.LanguageProvider
import com.sopt.clody.presentation.utils.language.LanguageProviderImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
interface LanguageModule {

@Binds
fun bindLanguageProvider(
languageProviderImpl: LanguageProviderImpl,
): LanguageProvider
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@ import com.sopt.clody.ui.theme.ClodyTheme
fun NickNameTextField(
value: String,
onValueChange: (String) -> Unit,
maxLength: Int,
isFocused: Boolean,
isValid: Boolean,
onRemove: () -> Unit,
onFocusChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
hint: String = "",
) {
val maxLength = 10

BasicTextField(
value = value,
onValueChange = {
Expand Down Expand Up @@ -102,6 +101,7 @@ fun PreviewNickNameTextField() {
NickNameTextField(
value = "닉네임",
onValueChange = {},
maxLength = 15,
isFocused = false,
isValid = true,
onRemove = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ class SignUpContract {
val nickname: String = "",
val isNicknameFocused: Boolean = false,
val isValidNickname: Boolean = true,
val nicknameMaxLength: Int = 15,
val nicknameMessage: String = DEFAULT_NICKNAME_MESSAGE,
val isLoading: Boolean = false,
val errorMessage: String? = null,
val serviceChecked: Boolean = false,
val serviceUrl: String = "",
val privacyChecked: Boolean = false,
val privacyUrl: String = "",
) : MavericksState {
val allChecked: Boolean
get() = serviceChecked && privacyChecked
Expand All @@ -26,13 +29,15 @@ class SignUpContract {
sealed class SignUpIntent {
data class SetNickname(val value: String) : SignUpIntent()
data class SetNicknameFocus(val isFocused: Boolean) : SignUpIntent()
data object SetNicknameMaxLength : SignUpIntent()
data object ProceedTerms : SignUpIntent()
data class CompleteSignUp(val context: Context) : SignUpIntent()
data object ClearError : SignUpIntent()

data class ToggleAllChecked(val checked: Boolean) : SignUpIntent()
data class ToggleServiceChecked(val checked: Boolean) : SignUpIntent()
data class TogglePrivacyChecked(val checked: Boolean) : SignUpIntent()
data object SetWebViewUrl : SignUpIntent()
data class OpenWebView(val url: String) : SignUpIntent()
data object BackToTerms : SignUpIntent()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ fun SignUpScreen(
allChecked = state.allChecked,
serviceChecked = state.serviceChecked,
privacyChecked = state.privacyChecked,
serviceUrl = state.serviceUrl,
privacyUrl = state.privacyUrl,
onToggleAll = { onIntent(SignUpContract.SignUpIntent.ToggleAllChecked(it)) },
onToggleService = { onIntent(SignUpContract.SignUpIntent.ToggleServiceChecked(it)) },
onTogglePrivacy = { onIntent(SignUpContract.SignUpIntent.TogglePrivacyChecked(it)) },
onAgreeClick = { onIntent(SignUpContract.SignUpIntent.ProceedTerms) },
navigateToPrevious = navigateToPrevious,
navigateToWebView = { url ->
onIntent(SignUpContract.SignUpIntent.OpenWebView(url))
},
navigateToWebView = { url -> onIntent(SignUpContract.SignUpIntent.OpenWebView(url)) },
)
}

Expand All @@ -87,6 +87,7 @@ fun SignUpScreen(
onBackClick = { onIntent(SignUpContract.SignUpIntent.BackToTerms) },
isLoading = state.isLoading,
isValidNickname = state.isValidNickname,
nicknameMaxLength = state.nicknameMaxLength,
nicknameMessage = state.nicknameMessage,
isFocused = state.isNicknameFocused,
onFocusChanged = { onIntent(SignUpContract.SignUpIntent.SetNicknameFocus(it)) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.sopt.clody.data.remote.util.NetworkUtil
import com.sopt.clody.domain.repository.AuthRepository
import com.sopt.clody.domain.repository.TokenRepository
import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE
import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls
import com.sopt.clody.presentation.utils.language.LanguageProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
Expand All @@ -30,6 +32,7 @@ class SignUpViewModel @AssistedInject constructor(
private val tokenRepository: TokenRepository,
private val fcmTokenProvider: FcmTokenProvider,
private val networkUtil: NetworkUtil,
private val languageProvider: LanguageProvider,
) : MavericksViewModel<SignUpContract.SignUpState>(initialState) {

private val _intents = Channel<SignUpContract.SignUpIntent>(BUFFERED)
Expand All @@ -41,6 +44,8 @@ class SignUpViewModel @AssistedInject constructor(
.receiveAsFlow()
.onEach(::handleIntent)
.launchIn(viewModelScope)
postIntent(SignUpContract.SignUpIntent.SetNicknameMaxLength)
postIntent(SignUpContract.SignUpIntent.SetWebViewUrl)
}

fun postIntent(intent: SignUpContract.SignUpIntent) {
Expand All @@ -51,12 +56,14 @@ class SignUpViewModel @AssistedInject constructor(
when (intent) {
is SignUpContract.SignUpIntent.SetNickname -> handleSetNickname(intent)
is SignUpContract.SignUpIntent.SetNicknameFocus -> handleSetNicknameFocus(intent)
is SignUpContract.SignUpIntent.SetNicknameMaxLength -> setNicknameMaxLength()
is SignUpContract.SignUpIntent.ProceedTerms -> handleProceedTerms()
is SignUpContract.SignUpIntent.CompleteSignUp -> signUp(intent.context)
is SignUpContract.SignUpIntent.ClearError -> clearError()
is SignUpContract.SignUpIntent.ToggleAllChecked -> handleToggleAllChecked(intent)
is SignUpContract.SignUpIntent.ToggleServiceChecked -> handleToggleServiceChecked(intent)
is SignUpContract.SignUpIntent.TogglePrivacyChecked -> handleTogglePrivacyChecked(intent)
is SignUpContract.SignUpIntent.SetWebViewUrl -> setWebViewUrl()
is SignUpContract.SignUpIntent.OpenWebView -> handleOpenWebView(intent.url)
SignUpContract.SignUpIntent.BackToTerms -> handleBackToTerms()
}
Expand All @@ -81,6 +88,10 @@ class SignUpViewModel @AssistedInject constructor(
setState { copy(isNicknameFocused = intent.isFocused) }
}

private fun setNicknameMaxLength() {
setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) }
}

private fun handleProceedTerms() {
setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) }
}
Expand All @@ -103,6 +114,15 @@ class SignUpViewModel @AssistedInject constructor(
setState { copy(privacyChecked = intent.checked) }
}

private fun setWebViewUrl() {
setState {
copy(
serviceUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.TERMS_OF_SERVICE_URL),
privacyUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.PRIVACY_POLICY_URL),
)
}
}

private suspend fun handleOpenWebView(url: String) {
_sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToWebView(url))
}
Expand Down Expand Up @@ -153,7 +173,9 @@ class SignUpViewModel @AssistedInject constructor(
}

private fun validateNickname(nickname: String): Boolean {
val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$".toRegex()
val state = withState(this@SignUpViewModel) { it }
setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) }
val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}$".toRegex()
return nickname.matches(regex)
}
Comment on lines 175 to 180
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize validation logic to avoid redundant operations.

The current implementation has two issues:

  1. setState call on line 177 is redundant since setNicknameMaxLength() is already called during initialization
  2. Regex is reconstructed on every validation call, which is inefficient

Apply this diff to improve performance:

 private fun validateNickname(nickname: String): Boolean {
-    val state = withState(this@SignUpViewModel) { it }
-    setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) }
-    val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}$".toRegex()
+    val state = withState(this@SignUpViewModel) { it }
+    val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}}$".toRegex()
     return nickname.matches(regex)
 }

Consider caching the regex pattern or moving validation logic to a separate class for better performance and testability.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun validateNickname(nickname: String): Boolean {
val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$".toRegex()
val state = withState(this@SignUpViewModel) { it }
setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) }
val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}$".toRegex()
return nickname.matches(regex)
}
private fun validateNickname(nickname: String): Boolean {
val state = withState(this@SignUpViewModel) { it }
val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}}$".toRegex()
return nickname.matches(regex)
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt
around lines 175 to 180, remove the redundant setState call that updates
nicknameMaxLength since it is already set during initialization. Also, avoid
reconstructing the regex pattern on every validation call by caching the regex
as a class-level property or moving the validation logic to a separate class
where the regex can be compiled once and reused, improving performance and
testability.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.sopt.clody.ui.theme.ClodyTheme
fun NickNamePage(
nickname: String,
isValidNickname: Boolean,
nicknameMaxLength: Int,
nicknameMessage: String,
isLoading: Boolean,
isFocused: Boolean,
Expand All @@ -56,7 +57,7 @@ fun NickNamePage(
append(" / ")
}
withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) {
append("10")
append("$nicknameMaxLength")
}
}

Expand Down Expand Up @@ -104,6 +105,7 @@ fun NickNamePage(
NickNameTextField(
value = nickname,
onValueChange = onNicknameChange,
maxLength = nicknameMaxLength,
hint = stringResource(R.string.nickname_input_hint),
isFocused = isFocused,
isValid = isValidNickname,
Expand Down Expand Up @@ -146,6 +148,7 @@ private fun NicknamePagePreview() {
NickNamePage(
nickname = "클로디",
isValidNickname = true,
nicknameMaxLength = 15,
nicknameMessage = "사용 가능한 닉네임입니다.",
isLoading = false,
isFocused = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ import com.sopt.clody.R
import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox
import com.sopt.clody.presentation.ui.component.button.ClodyButton
import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider
import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls
import com.sopt.clody.presentation.utils.base.BasePreview
import com.sopt.clody.presentation.utils.base.ClodyPreview
import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage
import com.sopt.clody.ui.theme.ClodyTheme
import java.util.Locale

@Composable
fun TermsOfServicePage(
allChecked: Boolean,
serviceChecked: Boolean,
privacyChecked: Boolean,
serviceUrl: String,
privacyUrl: String,
onToggleAll: (Boolean) -> Unit,
onToggleService: (Boolean) -> Unit,
onTogglePrivacy: (Boolean) -> Unit,
Expand All @@ -46,10 +46,6 @@ fun TermsOfServicePage(
navigateToWebView: (String) -> Unit,
) {
val isAgreeButtonEnabled = serviceChecked && privacyChecked
val currentLang = Locale.getDefault().language

val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.krUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl
val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.krUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl

Scaffold(
topBar = {
Expand Down Expand Up @@ -117,14 +113,14 @@ fun TermsOfServicePage(
text = stringResource(R.string.terms_service_use),
checked = serviceChecked,
onCheckedChange = onToggleService,
onClickMore = { navigateToWebView(termsOfService) },
onClickMore = { navigateToWebView(serviceUrl) },
)
Spacer(modifier = Modifier.height(24.dp))
TermsCheckboxRow(
text = stringResource(R.string.terms_service_privacy),
checked = privacyChecked,
onCheckedChange = onTogglePrivacy,
onClickMore = { navigateToWebView(privacyPolicy) },
onClickMore = { navigateToWebView(privacyUrl) },
)
}
},
Expand Down Expand Up @@ -169,6 +165,8 @@ private fun TermsOfServicePagePreview() {
allChecked = false,
serviceChecked = false,
privacyChecked = false,
serviceUrl = "",
privacyUrl = "",
onToggleAll = {},
onToggleService = {},
onTogglePrivacy = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import com.airbnb.mvrx.MavericksState
class LoginContract {

data class LoginState(
val loginType: LoginType = LoginType.GOOGLE,
val isLoading: Boolean = false,
val errorMessage: String? = null,
) : MavericksState

sealed class LoginIntent {
data object SetLoginType : LoginIntent()
data class LoginWithKakao(val context: Context) : LoginIntent()
data class LoginWithGoogle(val context: Context) : LoginIntent()
data object ClearError : LoginIntent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.sopt.clody.R
import com.sopt.clody.presentation.ui.auth.component.button.GoogleButton
import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton
import com.sopt.clody.presentation.ui.component.LoadingScreen
import com.sopt.clody.presentation.ui.component.dialog.FailureDialog
Expand All @@ -32,7 +33,6 @@ import com.sopt.clody.presentation.utils.base.ClodyPreview
import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage
import com.sopt.clody.presentation.utils.extension.repeatOnStarted
import com.sopt.clody.ui.theme.ClodyTheme
import java.util.Locale

@Composable
fun LoginRoute(
Expand All @@ -59,7 +59,7 @@ fun LoginRoute(
}

LoginScreen(
isLoading = state.isLoading,
state = state,
onKaKaoLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) },
onGoogleLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithGoogle(context)) },
)
Expand All @@ -74,13 +74,12 @@ fun LoginRoute(

@Composable
fun LoginScreen(
isLoading: Boolean,
state: LoginContract.LoginState,
onKaKaoLoginClick: () -> Unit,
onGoogleLoginClick: () -> Unit,
) {
val systemUiController = rememberSystemUiController()
val backgroundColor = ClodyTheme.colors.white
val currentLang = Locale.getDefault().language

LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(
Expand All @@ -91,15 +90,27 @@ fun LoginScreen(

Scaffold(
bottomBar = {
KaKaoButton(
text = stringResource(id = R.string.signup_btn_kakao),
onClick = onKaKaoLoginClick,
modifier = Modifier
.navigationBarsPadding()
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 40.dp),
)
if (state.loginType == LoginType.KAKAO) {
KaKaoButton(
text = stringResource(id = R.string.signup_btn_kakao),
onClick = onKaKaoLoginClick,
modifier = Modifier
.navigationBarsPadding()
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 40.dp),
)
} else {
GoogleButton(
text = stringResource(id = R.string.signup_btn_google),
onClick = onGoogleLoginClick,
modifier = Modifier
.navigationBarsPadding()
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 40.dp),
)
}
},
) { innerPadding ->
Column(
Expand All @@ -118,7 +129,7 @@ fun LoginScreen(
}
}

if (isLoading) {
if (state.isLoading) {
LoadingScreen()
}
}
Expand All @@ -128,7 +139,7 @@ fun LoginScreen(
fun LoginScreenPreview() {
BasePreview {
LoginScreen(
isLoading = false,
state = LoginContract.LoginState(),
onKaKaoLoginClick = {},
onGoogleLoginClick = {},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sopt.clody.presentation.ui.login

enum class LoginType {
KAKAO, GOOGLE
}
Loading