From 819fa9f557c3e9a23e29bbd4a7ec6216aa6a1d47 Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Fri, 15 Nov 2024 20:58:51 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[Design]=20-=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=95=84=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/and/components/SignButton.kt | 4 +- .../org/sopt/and/components/SignTextField.kt | 9 +- .../main/java/org/sopt/and/myinfo/MyScreen.kt | 2 +- .../java/org/sopt/and/signup/SignUpScreen.kt | 104 ++++++++++++++---- .../org/sopt/and/viewmodel/SignViewModel.kt | 38 +++---- app/src/main/res/values/strings.xml | 6 + 6 files changed, 117 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/sopt/and/components/SignButton.kt b/app/src/main/java/org/sopt/and/components/SignButton.kt index d89c54f1..3d93b97f 100644 --- a/app/src/main/java/org/sopt/and/components/SignButton.kt +++ b/app/src/main/java/org/sopt/and/components/SignButton.kt @@ -16,7 +16,7 @@ fun AuthSignButton( buttonText: String, validateAction: () -> Boolean, onSuccess: () -> Unit, - onFailure: () -> Unit, + onFailure: () -> Unit, // @Composable 제거 modifier: Modifier = Modifier, buttonColor: Color = Color.Blue ) { @@ -28,7 +28,7 @@ fun AuthSignButton( if (validateAction()) { onSuccess() } else { - onFailure() + onFailure() // 일반 함수 호출 } } }, diff --git a/app/src/main/java/org/sopt/and/components/SignTextField.kt b/app/src/main/java/org/sopt/and/components/SignTextField.kt index 7293b7d0..c81538f6 100644 --- a/app/src/main/java/org/sopt/and/components/SignTextField.kt +++ b/app/src/main/java/org/sopt/and/components/SignTextField.kt @@ -10,19 +10,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.sp @Composable fun CustomTextField( labelResId: Int, - textValue: String, - onTextChanged: (String) -> Unit, + textValue: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit, isPasswordField: Boolean = false, isPasswordVisible: Boolean = false, onPasswordToggle: (() -> Unit)? = null, - showHint: Boolean = false, // 추가된 파라미터 - hintResId: Int? = null, // 추가된 파라미터 + showHint: Boolean = false, + hintResId: Int? = null, modifier: Modifier = Modifier ) { Column { diff --git a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt b/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt index 0c6546fb..3819cb76 100644 --- a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt +++ b/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt @@ -32,7 +32,7 @@ fun MyScreen(modifier: Modifier = Modifier, signViewModel: SignViewModel) { .fillMaxSize() .background(Color.Black) ) { - MyHeader(email = signViewModel.email) + MyHeader(email = signViewModel.email.toString()) Spacer(modifier = Modifier.height(20.dp)) PurchseZone(title = "첫 결제 시 첫 달 100원!") Spacer(modifier = Modifier.height(15.dp)) diff --git a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt index ab31e037..d28e8888 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt @@ -4,25 +4,23 @@ import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text +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.platform.LocalContext +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch import org.sopt.and.R import org.sopt.and.components.AuthSignButton import org.sopt.and.viewmodel.SignViewModel import org.sopt.and.components.CustomTextField import org.sopt.and.components.SignTopBar - @Composable fun SignUpScreen( signViewModel: SignViewModel, @@ -30,6 +28,16 @@ fun SignUpScreen( ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + // 상태값 관리 + var username by remember { mutableStateOf(TextFieldValue("")) } + var password by remember { mutableStateOf(TextFieldValue("")) } + var hobby by remember { mutableStateOf(TextFieldValue("")) } + + var usernameError by remember { mutableStateOf(false) } + var passwordError by remember { mutableStateOf(false) } + var hobbyError by remember { mutableStateOf(false) } Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } @@ -46,50 +54,108 @@ fun SignUpScreen( SignTopBar(isSignUp = true) Spacer(modifier = Modifier.height(30.dp)) + // Username 입력 필드 CustomTextField( - labelResId = R.string.email_label, - textValue = signViewModel.email, - onTextChanged = { signViewModel.email = it }, + labelResId = R.string.username_label, + textValue = username, + onTextChanged = { + username = it + usernameError = username.text.length < 8 + }, showHint = true, - hintResId = R.string.sign_up_id, + hintResId = R.string.sign_up_username_hint, modifier = Modifier.fillMaxWidth() ) + if (usernameError) { + Text( + text = "Username must be at least 8 characters.", + color = Color.Red, + modifier = Modifier.padding(top = 4.dp) + ) + } Spacer(modifier = Modifier.height(10.dp)) + // Password 입력 필드 CustomTextField( labelResId = R.string.password_label, - textValue = signViewModel.password, - onTextChanged = { signViewModel.password = it }, + textValue = password, + onTextChanged = { + password = it + passwordError = password.text.length < 8 + }, isPasswordField = true, isPasswordVisible = signViewModel.isPasswordVisible, onPasswordToggle = { signViewModel.isPasswordVisible = !signViewModel.isPasswordVisible }, showHint = true, - hintResId = R.string.sign_up_passwd, + hintResId = R.string.sign_up_password_hint, modifier = Modifier.fillMaxWidth() ) + if (passwordError) { + Text( + text = "Password must be at least 8 characters.", + color = Color.Red, + modifier = Modifier.padding(top = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + // Hobby 입력 필드 + CustomTextField( + labelResId = R.string.hobby_label, + textValue = hobby, + onTextChanged = { + hobby = it + hobbyError = hobby.text.length < 8 + }, + showHint = true, + hintResId = R.string.sign_up_hobby_hint, + modifier = Modifier.fillMaxWidth() + ) + if (hobbyError) { + Text( + text = "Hobby must be at least 8 characters.", + color = Color.Red, + modifier = Modifier.padding(top = 4.dp) + ) + } Spacer(modifier = Modifier.weight(1f)) + // 회원가입 버튼 AuthSignButton( - buttonText = "Wavve 회원가입", - validateAction = { signViewModel.validateSignInOrUp() }, + buttonText = "회원가입", + validateAction = { + // 전체 유효성 검사 + usernameError = username.text.length < 8 + passwordError = password.text.length < 8 + hobbyError = hobby.text.length < 8 + + // 모든 조건을 만족해야 회원가입 요청 실행 + !usernameError && !passwordError && !hobbyError + }, onSuccess = { - signViewModel.performSignUp() + signViewModel.performSignUp( + username.text, + password.text, + hobby.text + ) Toast.makeText(context, "회원가입 성공!", Toast.LENGTH_SHORT).show() onNavigateToSignIn() }, onFailure = { - Toast.makeText(context, "회원가입 실패: 입력 정보를 확인해주세요.", Toast.LENGTH_SHORT).show() + coroutineScope.launch { + snackbarHostState.showSnackbar("회원가입 실패: 입력 정보를 확인해주세요.") + } } ) } } } - @Composable fun SignUpHeader(onNavigateToSignIn: () -> Unit) { Box( @@ -112,6 +178,4 @@ fun SignUpHeader(onNavigateToSignIn: () -> Unit) { .clickable { onNavigateToSignIn() } ) } -} - - +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt index 55a0c182..be5962a6 100644 --- a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt +++ b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt @@ -6,6 +6,7 @@ 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 kotlinx.coroutines.Dispatchers @@ -17,48 +18,49 @@ class SignViewModel(application: Application) : AndroidViewModel(application) { application.getSharedPreferences("user_prefs", Application.MODE_PRIVATE) } - var email by mutableStateOf("") - var password by mutableStateOf("") + var email by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용 + var password by mutableStateOf(TextFieldValue("")) // TextFieldValue 사용 var isPasswordVisible by mutableStateOf(false) private var emailError by mutableStateOf("") private var passwordError by mutableStateOf("") - /** Sign-up: Save email and password to SharedPreferences */ - fun performSignUp() { - if (validateSignInOrUp()) { + fun performSignUp(username: String, password: String, hobby: String) { + if (validateSignInOrUp(username, password, hobby)) { viewModelScope.launch(Dispatchers.IO) { preferences.edit().apply { - putString("saved_email", email) + putString("saved_username", username) putString("saved_password", password) + putString("saved_hobby", hobby) apply() } - Log.d("SignViewModel", "Email and Password saved: $email, $password") + Log.d("SignViewModel", "User info saved: Username: ${username}, Password: ${password}, Hobby: ${hobby}") } } else { - Log.d("SignViewModel", "Validation failed. Email: $email, Password: $password") + Log.d("SignViewModel", "Validation failed. Username: ${username}, Password: ${password}, Hobby: ${hobby}") } } + /** Validate email and password during sign-in */ fun validateSignIn(): Boolean { val savedEmail = preferences.getString("saved_email", "") val savedPassword = preferences.getString("saved_password", "") Log.d("SignViewModel", "Loaded Saved Email: $savedEmail, Saved Password: $savedPassword") - return email == savedEmail && password == savedPassword + return email.text == savedEmail && password.text == savedPassword // 수정: TextFieldValue에서 text 접근 } /** Validates both email and password */ - fun validateSignInOrUp(): Boolean { - return isEmailValid() && isPasswordValid() + fun validateSignInOrUp(username: String, password: String, hobby: String): Boolean { + return username.length >= 8 && password.length >= 8 && hobby.length >= 8 } /** Checks if the email meets the required format */ private fun isEmailValid(): Boolean { emailError = when { - email.isEmpty() -> "이메일을 입력하세요." - !Constants.EMAIL_REGEX.matches(email) -> "이메일 형식이 올바르지 않습니다." + email.text.isEmpty() -> "이메일을 입력하세요." + !Constants.EMAIL_REGEX.matches(email.text) -> "이메일 형식이 올바르지 않습니다." else -> "" } return emailError.isEmpty() @@ -67,19 +69,16 @@ class SignViewModel(application: Application) : AndroidViewModel(application) { /** Checks if the password meets length and complexity requirements */ private fun isPasswordValid(): Boolean { passwordError = when { - password.isEmpty() -> "비밀번호를 입력하세요." - password.length !in Constants.MIN_PASSWORD_LENGTH..Constants.MAX_PASSWORD_LENGTH -> + password.text.isEmpty() -> "비밀번호를 입력하세요." + password.text.length !in Constants.MIN_PASSWORD_LENGTH..Constants.MAX_PASSWORD_LENGTH -> "비밀번호는 ${Constants.MIN_PASSWORD_LENGTH}-${Constants.MAX_PASSWORD_LENGTH}자여야 합니다." - !isPasswordComplexEnough(password) -> + !isPasswordComplexEnough(password.text) -> "비밀번호는 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다." else -> "" } return passwordError.isEmpty() } - /** Helper to check if password meets complexity requirements */ - - companion object Constants { const val MIN_PASSWORD_LENGTH = 8 const val MAX_PASSWORD_LENGTH = 20 @@ -90,6 +89,7 @@ class SignViewModel(application: Application) : AndroidViewModel(application) { val DIGIT_REGEX = Regex("[0-9]") val SPECIAL_REGEX = Regex("[!@#\$%^&*(),.?\\\":{}|<>]") } + private fun isPasswordComplexEnough(password: String): Boolean { val criteriaCount = listOf( Constants.LOWER_CASE_REGEX.containsMatchIn(password), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a481e718..bb5a9d97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,4 +14,10 @@ 오늘의 TOP 20 비밀번호는 8~20자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해주세요 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요. + 아이디를 입력해주세요! + 아이디를 입력해주세요. + 비밀번호를 입력해주세요. + 취미를 입력해주세요. + 취미를 입력해주세요. + \ No newline at end of file From e7a058c23627578162679c20ed46cb9f0d1b8216 Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Fri, 15 Nov 2024 21:05:43 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[Feature]=20-=20BASE=20URL=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/org/sopt/and/api/api.kt | 60 +++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/src/main/java/org/sopt/and/api/api.kt diff --git a/app/src/main/java/org/sopt/and/api/api.kt b/app/src/main/java/org/sopt/and/api/api.kt new file mode 100644 index 00000000..36ac5ad1 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/api.kt @@ -0,0 +1,60 @@ +package org.sopt.and.api + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.and.BuildConfig +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ApiFactory { + private const val BASE_URL: String = BuildConfig.BASE_URL + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + return OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + } + + @Provides + @Singleton + fun provideRetrofit(client: OkHttpClient): Retrofit { + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + @Provides + @Singleton + fun provideUserRegistrationService(retrofit: Retrofit): UserRegistrationService { + return retrofit.create(UserRegistrationService::class.java) + } + + @Provides + @Singleton + fun provideLoginService(retrofit: Retrofit): LoginService { + return retrofit.create(LoginService::class.java) + } + + @Provides + @Singleton + fun provideHobbyService(retrofit: Retrofit): HobbyService { + return retrofit.create(HobbyService::class.java) + } + +} \ No newline at end of file From b6c20a6bc2abae4bbc922fb9d3c93a840ba62ac7 Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Fri, 15 Nov 2024 21:08:23 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[Feature]=20-=20=ED=94=8C=EB=9F=AC=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=EC=B4=88=EA=B8=B0=EC=84=B8=ED=8C=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 20 +++++++++++++++++++- app/src/main/AndroidManifest.xml | 2 ++ gradle/libs.versions.toml | 14 ++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ae1b66e..13387cb5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,17 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) +} + +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) } + android { namespace = "org.sopt.and" compileSdk = 34 @@ -14,7 +22,7 @@ android { targetSdk = 34 versionCode = 1 versionName = "1.0" - + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -36,6 +44,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -52,6 +61,13 @@ dependencies { implementation(libs.androidx.espresso.core) implementation(libs.androidx.navigation.compose) + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization.converter) + implementation(libs.kotlinx.serialization.json) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -59,4 +75,6 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e3baa05..3149eaa6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + Date: Fri, 15 Nov 2024 22:58:41 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[Feature]=20-=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 37 ++++++---- .../main/java/org/sopt/and/MainActivity.kt | 6 ++ .../java/org/sopt/and/api/{api.kt => Api.kt} | 0 app/src/main/java/org/sopt/and/api/Auth.kt | 25 +++++++ app/src/main/java/org/sopt/and/api/Hobby.kt | 13 ++++ app/src/main/java/org/sopt/and/dto/Auth.kt | 48 +++++++++++++ app/src/main/java/org/sopt/and/dto/My.kt | 18 +++++ .../java/org/sopt/and/signup/SignUpScreen.kt | 23 +++++-- .../org/sopt/and/viewmodel/SignViewModel.kt | 69 +++++++++++++------ build.gradle.kts | 9 ++- gradle/libs.versions.toml | 37 +++++++--- 11 files changed, 235 insertions(+), 50 deletions(-) rename app/src/main/java/org/sopt/and/api/{api.kt => Api.kt} (100%) create mode 100644 app/src/main/java/org/sopt/and/api/Auth.kt create mode 100644 app/src/main/java/org/sopt/and/api/Hobby.kt create mode 100644 app/src/main/java/org/sopt/and/dto/Auth.kt create mode 100644 app/src/main/java/org/sopt/and/dto/My.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 13387cb5..c7ef95bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,13 +5,15 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } + val properties = Properties().apply { load(project.rootProject.file("local.properties").inputStream()) } - android { namespace = "org.sopt.and" compileSdk = 34 @@ -22,8 +24,9 @@ android { targetSdk = 34 versionCode = 1 versionName = "1.0" - buildConfigField("String", "BASE_URL", properties["base.url"].toString()) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) } buildTypes { @@ -58,16 +61,8 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) - implementation(libs.androidx.espresso.core) - implementation(libs.androidx.navigation.compose) - - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp) - implementation(libs.okhttp.logging.interceptor) - implementation(libs.retrofit) - implementation(libs.retrofit.kotlin.serialization.converter) - implementation(libs.kotlinx.serialization.json) - + implementation(libs.androidx.navigation.runtime.ktx) + implementation(libs.androidx.runtime.livedata) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -75,6 +70,22 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.material) + // Network + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization.converter) + implementation(libs.kotlinx.serialization.json) + //hilt + implementation(libs.hilt.android.v2511) + kapt(libs.hilt.compiler.v2511) + implementation(libs.androidx.hilt.navigation.compose) +} - +kapt { + correctErrorTypes = true } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/MainActivity.kt b/app/src/main/java/org/sopt/and/MainActivity.kt index 36ddf3f1..6f757cbc 100644 --- a/app/src/main/java/org/sopt/and/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/MainActivity.kt @@ -18,6 +18,12 @@ import org.sopt.and.signin.SignInScreen import org.sopt.and.ui.theme.ANDANDROIDTheme import org.sopt.and.viewmodel.SignViewModel +import android.app.Application +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.HiltAndroidApp + + +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/sopt/and/api/api.kt b/app/src/main/java/org/sopt/and/api/Api.kt similarity index 100% rename from app/src/main/java/org/sopt/and/api/api.kt rename to app/src/main/java/org/sopt/and/api/Api.kt diff --git a/app/src/main/java/org/sopt/and/api/Auth.kt b/app/src/main/java/org/sopt/and/api/Auth.kt new file mode 100644 index 00000000..a26a42db --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/Auth.kt @@ -0,0 +1,25 @@ +package org.sopt.and.api + +import org.sopt.and.dto.RequestLoginData +import org.sopt.and.dto.RequestUserRegistrationData +import org.sopt.and.dto.ResponseLogin +import org.sopt.and.dto.ResponseUserRegistration +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + + +interface UserRegistrationService { + @POST("/user") + suspend fun postUserRegistration( + @Body userRequest: RequestUserRegistrationData + ): Response +} + +interface LoginService { + @POST("/login") + suspend fun postLogin( + @Body loginRequeset: RequestLoginData + ): Response +} + diff --git a/app/src/main/java/org/sopt/and/api/Hobby.kt b/app/src/main/java/org/sopt/and/api/Hobby.kt new file mode 100644 index 00000000..c1784eb0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/Hobby.kt @@ -0,0 +1,13 @@ +package org.sopt.and.api + +import org.sopt.and.dto.ResponseMyHobbyData +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Header + +interface HobbyService { + @GET("/user/my-hobby") + suspend fun getHobby( + @Header("token") token: String? + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/dto/Auth.kt b/app/src/main/java/org/sopt/and/dto/Auth.kt new file mode 100644 index 00000000..f26a1496 --- /dev/null +++ b/app/src/main/java/org/sopt/and/dto/Auth.kt @@ -0,0 +1,48 @@ +package org.sopt.and.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/* 유저 등록 */ +@Serializable +data class RequestUserRegistrationData( + @SerialName("username") + val userName: String, + @SerialName("password") + val password: String, + @SerialName("hobby") + val hobby: String +) + +@Serializable +data class ResponseUserRegistration( + @SerialName("result") + val result: ResultUserNo +) + +@Serializable +data class ResultUserNo( + @SerialName("no") + val no: Int +) + +/* 로그인 */ +@Serializable +data class RequestLoginData( + @SerialName("username") + val userName: String, + @SerialName("password") + val password: String +) + +@Serializable +data class ResponseLogin( + @SerialName("result") + val result: ResultToken +) + +@Serializable +data class ResultToken( + @SerialName("token") + val token: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/dto/My.kt b/app/src/main/java/org/sopt/and/dto/My.kt new file mode 100644 index 00000000..99e47962 --- /dev/null +++ b/app/src/main/java/org/sopt/and/dto/My.kt @@ -0,0 +1,18 @@ +package org.sopt.and.dto + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/* 내 취미 조회 */ +@Serializable +data class ResponseMyHobbyData( + @SerialName("result") + val result: ResponseMyHobbyDataResult +) + +@Serializable +data class ResponseMyHobbyDataResult( + @SerialName("hobby") + val hobby: String +) diff --git a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt index d28e8888..22aa6429 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt @@ -49,7 +49,7 @@ fun SignUpScreen( .padding(paddingValues) .padding(15.dp) ) { - SignUpHeader(onNavigateToSignIn) + SignUpTobBar (onNavigateToSignIn) Spacer(modifier = Modifier.height(20.dp)) SignTopBar(isSignUp = true) Spacer(modifier = Modifier.height(30.dp)) @@ -141,14 +141,21 @@ fun SignUpScreen( signViewModel.performSignUp( username.text, password.text, - hobby.text + hobby.text, + onSuccess = { + Toast.makeText(context, "회원가입 성공", Toast.LENGTH_SHORT).show() + onNavigateToSignIn() + }, + onFailure = { errorMessage -> + coroutineScope.launch { + snackbarHostState.showSnackbar(errorMessage) + } + } ) - Toast.makeText(context, "회원가입 성공!", Toast.LENGTH_SHORT).show() - onNavigateToSignIn() }, onFailure = { coroutineScope.launch { - snackbarHostState.showSnackbar("회원가입 실패: 입력 정보를 확인해주세요.") + snackbarHostState.showSnackbar("입력 값을 확인해주세요.") } } ) @@ -157,7 +164,7 @@ fun SignUpScreen( } @Composable -fun SignUpHeader(onNavigateToSignIn: () -> Unit) { +fun SignUpTobBar(onNavigateToSignIn: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() @@ -178,4 +185,6 @@ fun SignUpHeader(onNavigateToSignIn: () -> Unit) { .clickable { onNavigateToSignIn() } ) } -} \ No newline at end of file +} + + diff --git a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt index be5962a6..403c74bc 100644 --- a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt +++ b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt @@ -2,17 +2,25 @@ package org.sopt.and.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.api.UserRegistrationService +import org.sopt.and.dto.RequestUserRegistrationData +import org.sopt.and.dto.ResponseUserRegistration +import javax.inject.Inject -class SignViewModel(application: Application) : AndroidViewModel(application) { +@HiltViewModel +class SignViewModel @Inject constructor( + application: Application, + private val userRegistrationService: UserRegistrationService // Hilt를 통한 주입 +) : AndroidViewModel(application) { private val preferences: SharedPreferences by lazy { application.getSharedPreferences("user_prefs", Application.MODE_PRIVATE) @@ -25,32 +33,53 @@ class SignViewModel(application: Application) : AndroidViewModel(application) { private var emailError by mutableStateOf("") private var passwordError by mutableStateOf("") - /** Sign-up: Save email and password to SharedPreferences */ - fun performSignUp(username: String, password: String, hobby: String) { - if (validateSignInOrUp(username, password, hobby)) { - viewModelScope.launch(Dispatchers.IO) { - preferences.edit().apply { - putString("saved_username", username) - putString("saved_password", password) - putString("saved_hobby", hobby) - apply() + /** 회원가입 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()}") } - Log.d("SignViewModel", "User info saved: Username: ${username}, Password: ${password}, Hobby: ${hobby}") + } catch (e: Exception) { + onFailure("에러 발생: ${e.localizedMessage}") } - } else { - Log.d("SignViewModel", "Validation failed. Username: ${username}, Password: ${password}, Hobby: ${hobby}") } } - /** Validate email and password during sign-in */ - fun validateSignIn(): Boolean { - val savedEmail = preferences.getString("saved_email", "") - val savedPassword = preferences.getString("saved_password", "") - Log.d("SignViewModel", "Loaded Saved Email: $savedEmail, Saved Password: $savedPassword") - return email.text == savedEmail && password.text == savedPassword // 수정: TextFieldValue에서 text 접근 + + fun String.performSignUp( + password: String, + hobby: String, + onSuccess: (ResponseUserRegistration) -> Unit, + onFailure: (String) -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + try { + val request = RequestUserRegistrationData(this@performSignUp, 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}") + } + } } + /** Validates both email and password */ fun validateSignInOrUp(username: String, password: String, hobby: String): Boolean { return username.length >= 8 && password.length >= 8 && hobby.length >= 8 diff --git a/build.gradle.kts b/build.gradle.kts index 952b9306..8952fb35 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,11 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false -} \ No newline at end of file + id("com.google.dagger.hilt.android") version "2.51.1" apply false +} + +buildscript { + dependencies { + classpath(libs.hilt.android.gradle.plugin) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f7240c9..3ff8a150 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,39 @@ [versions] -agp = "8.7.0" +agp = "8.7.1" +hiltAndroid = "2.51.1" +hiltAndroidGradlePlugin = "2.38.1" +hiltCompiler = "2.51.1" +hiltNavigationCompose = "1.2.0" kotlin = "2.0.0" -coreKtx = "1.10.1" +coreKtx = "1.13.1" junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" -lifecycleRuntimeKtx = "2.6.1" -activityCompose = "1.8.0" -composeBom = "2024.04.01" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +lifecycleRuntimeKtx = "2.8.6" +activityCompose = "1.9.3" +composeBom = "2024.10.00" +lifecycleViewmodelCompose = "2.8.6" +material = "1.7.4" navigationCompose = "2.8.3" +navigationRuntimeKtx = "2.8.3" +# Third Party okhttp = "4.11.0" retrofit = "2.9.0" retrofitKotlinSerializationConverter = "1.0.0" kotlinxSerializationJson = "1.6.3" - +runtimeLivedata = "1.7.5" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidGradlePlugin" } +hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hiltAndroidGradlePlugin" } +hilt-android-v2511 = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroidGradlePlugin" } +hilt-compiler-v2511 = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -30,13 +47,15 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } +# Third Party okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From dd0fabb6975ea8e3a1fcc70e56269b62694654c8 Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Fri, 15 Nov 2024 23:30:50 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[Feature]=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../main/java/org/sopt/and/MainActivity.kt | 2 - .../main/java/org/sopt/and/MyApplication.kt | 7 +++ .../java/org/sopt/and/signin/SignInScreen.kt | 50 +++++++++++++++---- .../java/org/sopt/and/signup/SignUpScreen.kt | 42 +++++++++++----- .../org/sopt/and/viewmodel/SignViewModel.kt | 48 ++++++++++++------ 6 files changed, 112 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/org/sopt/and/MyApplication.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3149eaa6..54588e7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ Unit, onNavigateToSignUp: ()-> Unit) { +fun SignInScreen( + signViewModel: SignViewModel, + onNavigateToMain: () -> Unit, + onNavigateToSignUp: () -> Unit +) { val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + var isPasswordVisible by remember { mutableStateOf(false) } Scaffold( @@ -57,14 +64,42 @@ fun SignInScreen(signViewModel: SignViewModel, onNavigateToMain: () -> Unit, onN Spacer(modifier = Modifier.height(20.dp)) + // 로그인 버튼 AuthSignButton( buttonText = "로그인", - validateAction = { signViewModel.validateSignIn() }, + validateAction = { + // 입력 검증 + signViewModel.email.text.isNotEmpty() && signViewModel.password.text.isNotEmpty() + }, onSuccess = { - onNavigateToMain() + coroutineScope.launch { + signViewModel.performLogin( + onSuccess = { + // 로그인 성공 시 snackbar 호출 + coroutineScope.launch { + snackbarHostState.showSnackbar("로그인 성공!") + onNavigateToMain() + } + }, + onFailure = { errorMessage -> + // 로그인 실패 시 snackbar 호출 + coroutineScope.launch { + snackbarHostState.showSnackbar(errorMessage) + } + } + ) + } }, - onFailure = {} + onFailure = { + coroutineScope.launch { + snackbarHostState.showSnackbar("입력 값을 확인해주세요.") + } + } ) + + Spacer(modifier = Modifier.height(20.dp)) + + // 하단 링크 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -78,19 +113,16 @@ fun SignInScreen(signViewModel: SignViewModel, onNavigateToMain: () -> Unit, onN text = " | ", modifier = Modifier.padding(horizontal = 8.dp), color = Color.White - ) Text( text = "비밀번호 재설정", color = Color.White - ) Text( text = " | ", color = Color.White - ) Text( @@ -99,8 +131,6 @@ fun SignInScreen(signViewModel: SignViewModel, onNavigateToMain: () -> Unit, onN color = Color.White ) } - } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt index 22aa6429..a4ea66d2 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt @@ -14,7 +14,9 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import 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.viewmodel.SignViewModel @@ -28,6 +30,16 @@ fun SignUpScreen( ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } + + var snackbarMessage by remember { mutableStateOf(null) } + + LaunchedEffect(snackbarMessage) { + snackbarMessage?.let { + snackbarHostState.showSnackbar(it) + snackbarMessage = null // 메시지 초기화 + } + } + val coroutineScope = rememberCoroutineScope() // 상태값 관리 @@ -60,7 +72,7 @@ fun SignUpScreen( textValue = username, onTextChanged = { username = it - usernameError = username.text.length < 8 + usernameError = username.text.length > 8 }, showHint = true, hintResId = R.string.sign_up_username_hint, @@ -68,7 +80,7 @@ fun SignUpScreen( ) if (usernameError) { Text( - text = "Username must be at least 8 characters.", + text = "8자보다 크면 안됩니다.", color = Color.Red, modifier = Modifier.padding(top = 4.dp) ) @@ -82,7 +94,7 @@ fun SignUpScreen( textValue = password, onTextChanged = { password = it - passwordError = password.text.length < 8 + passwordError = password.text.length > 8 }, isPasswordField = true, isPasswordVisible = signViewModel.isPasswordVisible, @@ -95,7 +107,7 @@ fun SignUpScreen( ) if (passwordError) { Text( - text = "Password must be at least 8 characters.", + text = "8자보다 크면 안됩니다.", color = Color.Red, modifier = Modifier.padding(top = 4.dp) ) @@ -109,7 +121,7 @@ fun SignUpScreen( textValue = hobby, onTextChanged = { hobby = it - hobbyError = hobby.text.length < 8 + hobbyError = hobby.text.length > 8 }, showHint = true, hintResId = R.string.sign_up_hobby_hint, @@ -117,7 +129,7 @@ fun SignUpScreen( ) if (hobbyError) { Text( - text = "Hobby must be at least 8 characters.", + text = "8자보다 크면 안됩니다.", color = Color.Red, modifier = Modifier.padding(top = 4.dp) ) @@ -130,9 +142,7 @@ fun SignUpScreen( buttonText = "회원가입", validateAction = { // 전체 유효성 검사 - usernameError = username.text.length < 8 - passwordError = password.text.length < 8 - hobbyError = hobby.text.length < 8 + // 모든 조건을 만족해야 회원가입 요청 실행 !usernameError && !passwordError && !hobbyError @@ -143,19 +153,27 @@ fun SignUpScreen( password.text, hobby.text, onSuccess = { - Toast.makeText(context, "회원가입 성공", Toast.LENGTH_SHORT).show() + coroutineScope.launch { + withContext(Dispatchers.Main) { + snackbarHostState.showSnackbar("회원가입 성공") // 메인 스레드에서 호출 + } + } onNavigateToSignIn() }, onFailure = { errorMessage -> coroutineScope.launch { - snackbarHostState.showSnackbar(errorMessage) + withContext(Dispatchers.Main) { + snackbarHostState.showSnackbar(errorMessage) // 메인 스레드에서 호출 + } } } ) }, onFailure = { coroutineScope.launch { - snackbarHostState.showSnackbar("입력 값을 확인해주세요.") + withContext(Dispatchers.Main) { + snackbarHostState.showSnackbar("입력 값을 확인해주세요.") // 메인 스레드에서 호출 + } } } ) diff --git a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt index 403c74bc..40460cc3 100644 --- a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt +++ b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt @@ -2,6 +2,7 @@ package org.sopt.and.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 @@ -11,7 +12,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.sopt.and.api.LoginService import org.sopt.and.api.UserRegistrationService +import org.sopt.and.dto.RequestLoginData import org.sopt.and.dto.RequestUserRegistrationData import org.sopt.and.dto.ResponseUserRegistration import javax.inject.Inject @@ -19,7 +22,8 @@ import javax.inject.Inject @HiltViewModel class SignViewModel @Inject constructor( application: Application, - private val userRegistrationService: UserRegistrationService // Hilt를 통한 주입 + private val userRegistrationService: UserRegistrationService, // Hilt를 통한 주입 + private val loginService: LoginService // Hilt를 통한 주입 ) : AndroidViewModel(application) { private val preferences: SharedPreferences by lazy { @@ -56,33 +60,49 @@ class SignViewModel @Inject constructor( } } - /** Validate email and password during sign-in */ - - fun String.performSignUp( - password: String, - hobby: String, - onSuccess: (ResponseUserRegistration) -> Unit, + /** 로그인 API 요청 */ + fun performLogin( + onSuccess: () -> Unit, onFailure: (String) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { try { - val request = RequestUserRegistrationData(this@performSignUp, password, hobby) - val response = userRegistrationService.postUserRegistration(request) + 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 { onSuccess(it) } ?: onFailure("서버 응답이 비어있습니다.") + response.body()?.let { + val token = it.result.token + Log.d("SignViewModel", "로그인 성공, 토큰: $token") // 성공 응답 로그 + preferences.edit().putString("auth_token", token).apply() + onSuccess() + } ?: onFailure("서버 응답이 비어있습니다.") } else { - onFailure("회원가입 실패: ${response.code()} - ${response.message()}") + 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 + } - /** Validates both email and password */ - fun validateSignInOrUp(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() } /** Checks if the email meets the required format */ From 9e716f4c802d091c3d3a5e79c8bd897ef8953a52 Mon Sep 17 00:00:00 2001 From: sayyyho <323psh@naver.com> Date: Fri, 15 Nov 2024 23:51:24 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[Feature]=20-=20=EC=B7=A8=EB=AF=B8=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/myinfo/MyScreen.kt | 31 ++++++++- .../org/sopt/and/viewmodel/SignViewModel.kt | 64 +++++++++++-------- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt b/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt index 3819cb76..e614936d 100644 --- a/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt +++ b/app/src/main/java/org/sopt/and/myinfo/MyScreen.kt @@ -1,5 +1,7 @@ package org.sopt.and.myinfo +import android.annotation.SuppressLint +import android.util.Log import androidx.annotation.ColorRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -10,6 +12,12 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -17,6 +25,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch import org.sopt.and.viewmodel.SignViewModel enum class BottomNavItem(val icon: ImageVector, val description: String) { @@ -25,14 +34,30 @@ enum class BottomNavItem(val icon: ImageVector, val description: String) { PROFILE(Icons.Default.AccountCircle, "내 정보") } +@SuppressLint("UnrememberedMutableState") @Composable fun MyScreen(modifier: Modifier = Modifier, signViewModel: SignViewModel) { + var hobby by mutableStateOf("") // 초기 값 확인 + val coroutineScope = rememberCoroutineScope() + + // 취미 데이터를 로드 + LaunchedEffect(Unit) { + coroutineScope.launch { + signViewModel.fetchHobby( + onSuccess = { /* 성공 시 처리할 로직 필요 없음 - 이미 상태가 업데이트됨 */ }, + onFailure = { errorMessage -> + Log.e("MyScreen", "취미 로드 실패: $errorMessage") + } + ) + } + } + Column( modifier = Modifier .fillMaxSize() .background(Color.Black) ) { - MyHeader(email = signViewModel.email.toString()) + MyHeader(hobby = hobby) // 최신 hobby 값을 전달 Spacer(modifier = Modifier.height(20.dp)) PurchseZone(title = "첫 결제 시 첫 달 100원!") Spacer(modifier = Modifier.height(15.dp)) @@ -47,7 +72,7 @@ fun MyScreen(modifier: Modifier = Modifier, signViewModel: SignViewModel) { } @Composable -fun MyHeader(email: String) { +fun MyHeader(hobby: String) { Row( modifier = Modifier .fillMaxWidth() @@ -63,7 +88,7 @@ fun MyHeader(email: String) { tint = Color.White ) Text( - text = email, + text = hobby.ifEmpty { "sport" }, // 초기 값 및 업데이트된 값 반영 fontSize = 15.sp, color = Color.White, fontWeight = FontWeight.Bold, diff --git a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt index 40460cc3..833f4e08 100644 --- a/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt +++ b/app/src/main/java/org/sopt/and/viewmodel/SignViewModel.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.sopt.and.api.HobbyService import org.sopt.and.api.LoginService import org.sopt.and.api.UserRegistrationService import org.sopt.and.dto.RequestLoginData @@ -22,8 +23,9 @@ import javax.inject.Inject @HiltViewModel class SignViewModel @Inject constructor( application: Application, - private val userRegistrationService: UserRegistrationService, // Hilt를 통한 주입 - private val loginService: LoginService // Hilt를 통한 주입 + private val userRegistrationService: UserRegistrationService, // Hilt로 주입 + private val loginService: LoginService, // Hilt로 주입 + private val hobbyService: HobbyService // Hilt로 주입 ) : AndroidViewModel(application) { private val preferences: SharedPreferences by lazy { @@ -32,6 +34,7 @@ class SignViewModel @Inject constructor( 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("") @@ -95,6 +98,40 @@ class SignViewModel @Inject constructor( } } + /** 취미 조회 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 @@ -105,29 +142,6 @@ class SignViewModel @Inject constructor( return email.text.isNotEmpty() && password.text.isNotEmpty() } - /** Checks if the email meets the required format */ - private fun isEmailValid(): Boolean { - emailError = when { - email.text.isEmpty() -> "이메일을 입력하세요." - !Constants.EMAIL_REGEX.matches(email.text) -> "이메일 형식이 올바르지 않습니다." - else -> "" - } - return emailError.isEmpty() - } - - /** Checks if the password meets length and complexity requirements */ - private fun isPasswordValid(): Boolean { - passwordError = when { - password.text.isEmpty() -> "비밀번호를 입력하세요." - password.text.length !in Constants.MIN_PASSWORD_LENGTH..Constants.MAX_PASSWORD_LENGTH -> - "비밀번호는 ${Constants.MIN_PASSWORD_LENGTH}-${Constants.MAX_PASSWORD_LENGTH}자여야 합니다." - !isPasswordComplexEnough(password.text) -> - "비밀번호는 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다." - else -> "" - } - return passwordError.isEmpty() - } - companion object Constants { const val MIN_PASSWORD_LENGTH = 8 const val MAX_PASSWORD_LENGTH = 20