From 466c5c6f18f9446423db18a5b2579adfda5b96de Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Sun, 8 Dec 2024 18:58:00 +0900 Subject: [PATCH] =?UTF-8?q?[feature]=20#9=20-=20SignIn=20=EC=9E=AC?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/SignButton.kt | 2 +- .../components/SignTextField.kt | 2 +- .../components/SignTopBar.kt | 2 +- .../and/presentation/navigation/Navigation.kt | 10 +- .../and/presentation/signin/SignInScreen.kt | 14 +- .../and/presentation/signin/SignInUiState.kt | 14 ++ .../presentation/signin/SignInViewModel.kt | 130 ++++++++++++++ .../and/presentation/signup/SignUpScreen.kt | 6 +- .../org/sopt/and/presentation/util/Utils.kt | 47 +++++ .../presentation/viewmodel/SignViewModel.kt | 165 ------------------ .../SignInViewModelFactory.kt | 24 +++ .../SignUpViewModelFactory.kt | 24 +++ app/src/main/res/values/strings.xml | 78 ++++++++- 13 files changed, 337 insertions(+), 181 deletions(-) rename app/src/main/java/org/sopt/and/{ => presentation}/components/SignButton.kt (96%) rename app/src/main/java/org/sopt/and/{ => presentation}/components/SignTextField.kt (98%) rename app/src/main/java/org/sopt/and/{ => presentation}/components/SignTopBar.kt (97%) create mode 100644 app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/util/Utils.kt delete mode 100644 app/src/main/java/org/sopt/and/presentation/viewmodel/SignViewModel.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt create mode 100644 app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt diff --git a/app/src/main/java/org/sopt/and/components/SignButton.kt b/app/src/main/java/org/sopt/and/presentation/components/SignButton.kt similarity index 96% rename from app/src/main/java/org/sopt/and/components/SignButton.kt rename to app/src/main/java/org/sopt/and/presentation/components/SignButton.kt index 3d93b97f..91e9d4a7 100644 --- a/app/src/main/java/org/sopt/and/components/SignButton.kt +++ b/app/src/main/java/org/sopt/and/presentation/components/SignButton.kt @@ -1,4 +1,4 @@ -package org.sopt.and.components +package org.sopt.and.presentation.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/app/src/main/java/org/sopt/and/components/SignTextField.kt b/app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt similarity index 98% rename from app/src/main/java/org/sopt/and/components/SignTextField.kt rename to app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt index c81538f6..fbd3a7a6 100644 --- a/app/src/main/java/org/sopt/and/components/SignTextField.kt +++ b/app/src/main/java/org/sopt/and/presentation/components/SignTextField.kt @@ -1,4 +1,4 @@ -package org.sopt.and.components +package org.sopt.and.presentation.components import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text diff --git a/app/src/main/java/org/sopt/and/components/SignTopBar.kt b/app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt similarity index 97% rename from app/src/main/java/org/sopt/and/components/SignTopBar.kt rename to app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt index 605cb2e8..0c225e50 100644 --- a/app/src/main/java/org/sopt/and/components/SignTopBar.kt +++ b/app/src/main/java/org/sopt/and/presentation/components/SignTopBar.kt @@ -1,4 +1,4 @@ -package org.sopt.and.components +package org.sopt.and.presentation.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt b/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt index 063498ba..263af3aa 100644 --- a/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt +++ b/app/src/main/java/org/sopt/and/presentation/navigation/Navigation.kt @@ -51,8 +51,9 @@ fun Navigation( ) { composable { SignInScreen( - navigateToSignUp = { navController.navigate(route = Routes.SignUp) }, - navigateToMyInfo = { + onNavigateToSignUp = { navController.navigate(route = Routes.SignUp) }, + signViewModel = , + onNavigateToMain = { navigationViewModel.changeBottomNavigationVisibility() navController.navigate(Routes.MyInfo) } @@ -61,7 +62,8 @@ fun Navigation( composable { SignUpScreen( - navigateToSignIn = { + signViewModel = , + onNavigateToSignIn = { navController.navigate( route = Routes.SignIn, navOptions = navOptions { @@ -78,7 +80,7 @@ fun Navigation( MyScreen( paddingValues = innerPadding, myHobby = myInfoUiState.myHobby, - getMyHobby = myInfoViewModel::getMyHobby + getMyHobby = myInfoViewModel::getMyHobby, ) } diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt index 124a4b27..9eddd5e2 100644 --- a/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInScreen.kt @@ -8,21 +8,25 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.launch import org.sopt.and.R -import org.sopt.and.components.AuthSignButton -import org.sopt.and.presentation.viewmodel.SignViewModel +import org.sopt.and.presentation.components.AuthSignButton -import org.sopt.and.components.CustomTextField -import org.sopt.and.components.SignTopBar +import org.sopt.and.presentation.components.CustomTextField +import org.sopt.and.presentation.components.SignTopBar +import org.sopt.and.presentation.viewmodelfactory.SignUpViewModelFactory @Composable fun SignInScreen( - signViewModel: SignViewModel, onNavigateToMain: () -> Unit, onNavigateToSignUp: () -> Unit ) { + val signUpViewModel: SignUpViewModel = viewModel( + factory = SignUpViewModelFactory() + ) + val signUpUiState by signUpViewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt new file mode 100644 index 00000000..5398a6e0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInUiState.kt @@ -0,0 +1,14 @@ +package org.sopt.and.presentation.signin + +data class SignInUiState( + val signInUsername: String = "", + val signInPassword: String = "", + val isSignInPasswordVisible: Boolean = false +) + +sealed class SignInResult { + object Initial : SignInResult() + object Success : SignInResult() + object FailurePasswordLength : SignInResult() + object FailureWrongPassword : SignInResult() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt new file mode 100644 index 00000000..43de7afc --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signin/SignInViewModel.kt @@ -0,0 +1,130 @@ +package org.sopt.and.presentation.signin + +import android.content.Context +import androidx.compose.material3.SnackbarHostState +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.sopt.and.R +import org.sopt.and.data.service.AppContext +import org.sopt.and.data.service.TokenManager +import org.sopt.and.domain.model.SignInInformationEntity +import org.sopt.and.domain.model.SignInResponseEntity +import org.sopt.and.domain.usecase.SignInUseCase +import org.sopt.and.presentation.util.Utils.showSnackbar + +class SignInViewModel( + private val signInUseCase: SignInUseCase +) : ViewModel() { + private val tokenManager = TokenManager(AppContext.get()) + + private val _uiState = MutableStateFlow(SignInUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _signInResult = MutableStateFlow(SignInResult.Initial) + val signInResult: StateFlow = _signInResult.asStateFlow() + + private fun initSignInResult() { + _signInResult.value = SignInResult.Initial + } + + fun setSignInUsername(signInUsername: String) { + _uiState.value = _uiState.value.copy( + signInUsername = signInUsername + ) + } + + fun setSignInPassword(signInPassword: String) { + _uiState.value = _uiState.value.copy( + signInPassword = signInPassword + ) + } + + fun changeSignInPasswordVisibility() { + _uiState.value = _uiState.value.copy( + isSignInPasswordVisible = !_uiState.value.isSignInPasswordVisible + ) + } + + fun signIn( + signInUsername: String, + signInPassword: String + ) { + viewModelScope.launch { + signInUseCase( + request = SignInInformationEntity( + username = signInUsername, + password = signInPassword + ) + ).onSuccess { signInResponseEntity: SignInResponseEntity -> + if (signInResponseEntity.status == 200) { + _signInResult.value = SignInResult.Success + signInResponseEntity.token?.let { token -> + tokenManager.saveToken(token) + } + } else if (signInResponseEntity.code == SignInFailureCase.FAILURE_LENGTH.errorCode + && signInResponseEntity.status == SignInFailureCase.FAILURE_LENGTH.statusCode + ) { + _signInResult.value = SignInResult.FailurePasswordLength + } else if (signInResponseEntity.code == SignInFailureCase.FAILURE_WRONG_PASSWORD.errorCode + && signInResponseEntity.status == SignInFailureCase.FAILURE_WRONG_PASSWORD.statusCode + ) { + _signInResult.value = SignInResult.FailureWrongPassword + } + } + } + } + + fun confirmLogin( + snackbarHostState: SnackbarHostState, + navigateToMyInfo: () -> Unit, + context: Context, + scope: CoroutineScope + ) { + when (signInResult.value) { + is SignInResult.Success -> { + context.showSnackbar( + scope = scope, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_success_message, + ) + navigateToMyInfo() + initSignInResult() + } + + is SignInResult.FailurePasswordLength -> { + context.showSnackbar( + scope = scope, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_failed_password_length, + ) + initSignInResult() + } + + is SignInResult.FailureWrongPassword -> { + context.showSnackbar( + scope = scope, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_failed_wrong_password, + ) + initSignInResult() + } + + else -> {} + } + } +} + +data class SignInFailureCase( + val statusCode: Int, + val errorCode: String +) { + companion object { + val FAILURE_LENGTH = SignInFailureCase(statusCode = 400, errorCode = "01") + val FAILURE_WRONG_PASSWORD = SignInFailureCase(statusCode = 403, errorCode = "01") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt index 2303fda3..4a95316d 100644 --- a/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/signup/SignUpScreen.kt @@ -17,10 +17,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.sopt.and.R -import org.sopt.and.components.AuthSignButton +import org.sopt.and.presentation.components.AuthSignButton import org.sopt.and.presentation.viewmodel.SignViewModel -import org.sopt.and.components.CustomTextField -import org.sopt.and.components.SignTopBar +import org.sopt.and.presentation.components.CustomTextField +import org.sopt.and.presentation.components.SignTopBar @Composable fun SignUpScreen( diff --git a/app/src/main/java/org/sopt/and/presentation/util/Utils.kt b/app/src/main/java/org/sopt/and/presentation/util/Utils.kt new file mode 100644 index 00000000..62295669 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/util/Utils.kt @@ -0,0 +1,47 @@ +package org.sopt.and.presentation.util + +import android.content.Context +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.compose.material3.SnackbarHostState +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.sopt.and.R + +object Utils { + const val MIN_PASSWORD_LENGTH = 8 + const val MAX_PASSWORD_LENGTH = 20 + + const val MYINFO_SCREEN_INDEX = 2 + const val SEARCH_SCREEN_INDEX = 1 + const val HOME_SCREEN_INDEX = 0 + + const val GREETING_FIRST_LINE_FOCUS_START_INDEX = 0 + const val GREETING_FIRST_LINE_FOCUS_END_INDEX = 9 + const val GREETING_FIRST_LINE_END_INDEX = 12 + const val GREETING_SECOND_LINE_FOCUS_START_INDEX = 13 + const val GREETING_SECOND_LINE_FOCUS_END_INDEX = 24 + const val GREETING_SECOND_LINE_END_INDEX = 29 + + + fun transformationPasswordVisual(isVisible: Boolean): VisualTransformation = + if (isVisible) VisualTransformation.None else PasswordVisualTransformation() + + fun Context.showToast( + @StringRes message: Int + ) = Toast.makeText( + this, + this.getString(message), + Toast.LENGTH_SHORT + ).show() + + fun Context.showSnackbar( + scope: CoroutineScope, + snackbarHostState: SnackbarHostState, + @StringRes message: Int + ) = scope.launch { + snackbarHostState.showSnackbar(message = getString(message)) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/presentation/viewmodel/SignViewModel.kt deleted file mode 100644 index d383f0e6..00000000 --- a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignViewModel.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.sopt.and.presentation.viewmodel - -import android.app.Application -import android.content.SharedPreferences -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.sopt.and.data.api.HobbyService -import org.sopt.and.data.api.LoginService -import org.sopt.and.data.api.UserRegistrationService -import org.sopt.and.data.api.dto.RequestLoginData -import org.sopt.and.data.api.dto.RequestUserRegistrationData -import org.sopt.and.data.api.dto.ResponseUserRegistration -import javax.inject.Inject - -@HiltViewModel -class SignViewModel @Inject constructor( - application: Application, - private val userRegistrationService: UserRegistrationService, // Hilt로 주입 - private val loginService: LoginService, // Hilt로 주입 - private val hobbyService: HobbyService // Hilt로 주입 -) : AndroidViewModel(application) { - - private val preferences: SharedPreferences by lazy { - application.getSharedPreferences("user_prefs", Application.MODE_PRIVATE) - } - - var email by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용 - var password by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용 - var hobby by mutableStateOf("") // hobby 추가 - var isPasswordVisible by mutableStateOf(false) - - private var emailError by mutableStateOf("") - private var passwordError by mutableStateOf("") - - /** 회원가입 API 요청 */ - fun performSignUp( - username: String, - password: String, - hobby: String, - onSuccess: (ResponseUserRegistration) -> Unit, - onFailure: (String) -> Unit - ) { - viewModelScope.launch(Dispatchers.IO) { - try { - val request = RequestUserRegistrationData(username, password, hobby) - val response = userRegistrationService.postUserRegistration(request) // 주입된 서비스 사용 - if (response.isSuccessful) { - response.body()?.let { onSuccess(it) } ?: onFailure("서버 응답이 비어있습니다.") - } else { - onFailure("회원가입 실패: ${response.code()} - ${response.message()}") - } - } catch (e: Exception) { - onFailure("에러 발생: ${e.localizedMessage}") - } - } - } - - /** 로그인 API 요청 */ - fun performLogin( - onSuccess: () -> Unit, - onFailure: (String) -> Unit - ) { - viewModelScope.launch(Dispatchers.IO) { - try { - val request = RequestLoginData( - userName = email.text, - password = password.text - ) - Log.d("SignViewModel", "로그인 요청 데이터: $request") // 요청 데이터 로그 - - val response = loginService.postLogin(request) // Hilt로 주입된 서비스 사용 - if (response.isSuccessful) { - response.body()?.let { - val token = it.result.token - Log.d("SignViewModel", "로그인 성공, 토큰: $token") // 성공 응답 로그 - preferences.edit().putString("auth_token", token).apply() - onSuccess() - } ?: onFailure("서버 응답이 비어있습니다.") - } else { - Log.e( - "SignViewModel", - "로그인 실패: ${response.code()} - ${response.message()} - ${response.errorBody()?.string()}" - ) // 실패 로그 - onFailure("로그인 실패: ${response.code()} - ${response.message()}") - } - } catch (e: Exception) { - Log.e("SignViewModel", "로그인 요청 중 에러 발생: ${e.localizedMessage}", e) // 예외 로그 - onFailure("에러 발생: ${e.localizedMessage}") - } - } - } - - /** 취미 조회 API 요청 */ - fun fetchHobby( - onSuccess: () -> Unit, - onFailure: (String) -> Unit - ) { - val token = preferences.getString("auth_token", null) - if (token.isNullOrEmpty()) { - onFailure("유효한 토큰이 없습니다.") - return - } - - viewModelScope.launch(Dispatchers.IO) { - try { - val response = hobbyService.getHobby(token) - if (response.isSuccessful) { - response.body()?.let { - hobby = it.result.hobby // 상태값 업데이트 - Log.d("SignViewModel", "취미 조회 성공: $hobby") - onSuccess() - } ?: onFailure("서버 응답이 비어있습니다.") - } else { - Log.e( - "SignViewModel", - "취미 조회 실패: ${response.code()} - ${response.message()} - ${response.errorBody()?.string()}" - ) - onFailure("취미 조회 실패: ${response.code()} - ${response.message()}") - } - } catch (e: Exception) { - Log.e("SignViewModel", "취미 조회 요청 중 에러 발생: ${e.localizedMessage}", e) - onFailure("에러 발생: ${e.localizedMessage}") - } - } - } - - /** Validate inputs for sign-up */ - fun validateSignUpInputs(username: String, password: String, hobby: String): Boolean { - return username.length <= 8 && password.length <= 8 && hobby.length <= 8 - } - - /** Validate email and password during sign-in */ - fun validateSignInInputs(): Boolean { - return email.text.isNotEmpty() && password.text.isNotEmpty() - } - - companion object Constants { - const val MIN_PASSWORD_LENGTH = 8 - const val MAX_PASSWORD_LENGTH = 20 - const val PASSWORD_CRITERIA_COUNT = 3 - val EMAIL_REGEX = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$") - val LOWER_CASE_REGEX = Regex("[a-z]") - val UPPER_CASE_REGEX = Regex("[A-Z]") - val DIGIT_REGEX = Regex("[0-9]") - val SPECIAL_REGEX = Regex("[!@#\$%^&*(),.?\\\":{}|<>]") - } - - private fun isPasswordComplexEnough(password: String): Boolean { - val criteriaCount = listOf( - LOWER_CASE_REGEX.containsMatchIn(password), - UPPER_CASE_REGEX.containsMatchIn(password), - DIGIT_REGEX.containsMatchIn(password), - SPECIAL_REGEX.containsMatchIn(password) - ).count { it } - return criteriaCount >= PASSWORD_CRITERIA_COUNT - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt new file mode 100644 index 00000000..ed2c8ff9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignInViewModelFactory.kt @@ -0,0 +1,24 @@ +package org.sopt.and.presentation.viewmodelfactory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.sopt.and.domain.repository.SignInRepository +import org.sopt.and.domain.usecase.SignInUseCase +import org.sopt.and.presentation.signin.SignInViewModel + +class SignInViewModelFactory : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return when (modelClass) { + + SignInViewModel::class.java -> { + SignInViewModel( + SignInUseCase( + signInRepository = SignInRepository.create() + ) + ) as T + } + + else -> throw IllegalArgumentException("Unknown ViewModel Class") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt new file mode 100644 index 00000000..59c7ec86 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/viewmodelfactory/SignUpViewModelFactory.kt @@ -0,0 +1,24 @@ +package org.sopt.and.presentation.viewmodelfactory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.sopt.and.domain.repository.SignUpRepository +import org.sopt.and.domain.usecase.SignUpUseCase +import org.sopt.and.presentation.signup.SignUpViewModel + +class SignUpViewModelFactory : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return when (modelClass) { + + SignUpViewModel::class.java -> { + SignUpViewModel( + SignUpUseCase( + signUpRepository = SignUpRepository.create() + ) + ) as T + } + + else -> throw IllegalArgumentException("Unknown ViewModel Class") + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4471fb2f..ffa5b7b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - AND-ANDROID + 이메일 주소 또는 아이디 비밀번호 로그인 @@ -27,4 +27,80 @@ My + + wavve + | + hide + show + 카카오 계정과 연동하는 아이콘 + T World 계정과 연동하는 아이콘 + 네이버 계정과 연동하는 아이콘 + 페이스북 계정과 연동하는 아이콘 + 애플 계정과 연동하는 아이콘 + • SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다.\n + 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요. + + + + 회원가입 + wavve Username 설정 + wavve 비밀번호 설정 + 취미입력 ex)soccer + Wavve 회원가입 + 회원가입에 성공했습니다~ + Username이 중복되었습니다! + username, 비밀번호, 취미는 8자 이하로 입력해주세요! + 비밀번호는 8자 이하로 입력해 주세요 + Username는 8자 이하로 입력해 주세요 + Username 입력시 주의사항 + 비밀번호 입력시 주의사항 + 회원가입 화면 닫기 + 이메일과 비밀번호만으로\nWavve를 즐길 수 있어요! + 또는 다른 서비스 계정으로 가입 + + + 로그인 + 아이디 찾기 + 비밀번호 재설정 + 회원가입 + Username + 비밀번호 + 로그인 성공. 환영합니다~ + 로그인 실패. 비밀번호는 8자리 이하입니다! + 로그인 실패. 비밀번호가 일치하지 않습니다! + 이전 화면으로 가기 + 또는 다른 서비스 계정으로 로그인 + + + + 내 프로필 + 알림 + 설정 + 구매하기 > + 현재 보유하신 이용권이 없습니다. + 첫 결제 시 첫 달 100원! + 전체 시청내역 + 시청내역이 없어요. + 관심 프로그램 + 관심 프로그램이 없어요. + + + 믿고 보는 웨이브 에디터 추천작 + 오늘의 TOP 20 + %1$s  + | %1$s + + + + + + 뉴클래식 + 드라마 + 예능 + 영화 + 애니 + 해외시리즈 + %1$s + + \ No newline at end of file