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