diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd336bb..0bfa735 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,14 @@ +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 { @@ -16,6 +23,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String","BASE_URL", properties["base.url"].toString()) } buildTypes { @@ -36,6 +44,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -61,4 +70,11 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + // 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) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3f3d5d4..a185455 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ANDANDROID" + android:usesCleartextTraffic="true" tools:targetApi="31"> create(): T = retrofit.create(T::class.java) +} + +object ServicePool { + val userService = ApiFactory.create() + val loginService = ApiFactory.create() + val hobbyService = ApiFactory.create() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/dto/ErrorDto.kt b/app/src/main/java/org/sopt/and/api/dto/ErrorDto.kt new file mode 100644 index 0000000..7739cf9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/dto/ErrorDto.kt @@ -0,0 +1,10 @@ +package org.sopt.and.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseErrorDto( + @SerialName("code") + val code: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/dto/HobbyDto.kt b/app/src/main/java/org/sopt/and/api/dto/HobbyDto.kt new file mode 100644 index 0000000..ba29b3d --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/dto/HobbyDto.kt @@ -0,0 +1,16 @@ +package org.sopt.and.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseHobbySuccessDto( + @SerialName("result") + val result: HobbyData +) + +@Serializable +data class HobbyData( + @SerialName("hobby") + val hobby: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/dto/LoginDto.kt b/app/src/main/java/org/sopt/and/api/dto/LoginDto.kt new file mode 100644 index 0000000..d3af521 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/dto/LoginDto.kt @@ -0,0 +1,24 @@ +package org.sopt.and.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseLoginSuccessDto( + @SerialName("result") + val result: LoginData +) + +@Serializable +data class LoginData( + @SerialName("token") + val token: String +) + +@Serializable +data class RequestLoginDto( + @SerialName("username") + val username: String, + @SerialName("password") + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/dto/UserDto.kt b/app/src/main/java/org/sopt/and/api/dto/UserDto.kt new file mode 100644 index 0000000..f46daf1 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/dto/UserDto.kt @@ -0,0 +1,26 @@ +package org.sopt.and.api.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseUserSuccessDto( + @SerialName("result") + val result: UserData +) + +@Serializable +data class UserData( + @SerialName("no") + val no: Int +) + +@Serializable +data class RequestUserDto( + @SerialName("username") + val username: String, + @SerialName("password") + val password: String, + @SerialName("hobby") + val hobby: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/service/HobbyService.kt b/app/src/main/java/org/sopt/and/api/service/HobbyService.kt new file mode 100644 index 0000000..530a0f4 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/service/HobbyService.kt @@ -0,0 +1,13 @@ +package org.sopt.and.api.service + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Header + +interface HobbyService { + @GET("/user/my-hobby") + fun getMyHobby( + @Header("token") token: String + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/service/LoginService.kt b/app/src/main/java/org/sopt/and/api/service/LoginService.kt new file mode 100644 index 0000000..ef5390b --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/service/LoginService.kt @@ -0,0 +1,14 @@ +package org.sopt.and.api.service + +import okhttp3.ResponseBody +import org.sopt.and.api.dto.RequestLoginDto +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface LoginService { + @POST("/login") + fun postLogin( + @Body requestLogin: RequestLoginDto + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/api/service/UserService.kt b/app/src/main/java/org/sopt/and/api/service/UserService.kt new file mode 100644 index 0000000..2e20508 --- /dev/null +++ b/app/src/main/java/org/sopt/and/api/service/UserService.kt @@ -0,0 +1,14 @@ +package org.sopt.and.api.service + +import okhttp3.ResponseBody +import org.sopt.and.api.dto.RequestUserDto +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST + +interface UserService { + @POST("/user") + fun postUser( + @Body requestUser: RequestUserDto + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/component/BuyTextButton.kt b/app/src/main/java/org/sopt/and/component/BuyTextButton.kt index 7e63908..a432d77 100644 --- a/app/src/main/java/org/sopt/and/component/BuyTextButton.kt +++ b/app/src/main/java/org/sopt/and/component/BuyTextButton.kt @@ -18,7 +18,7 @@ import org.sopt.and.R @Composable fun BuyTextButton( labelText: String, - onClick: ()->Unit + onClick: () -> Unit ) { Column { Text( diff --git a/app/src/main/java/org/sopt/and/component/MyPageItem.kt b/app/src/main/java/org/sopt/and/component/MyPageItem.kt index 98c8f99..2fd6295 100644 --- a/app/src/main/java/org/sopt/and/component/MyPageItem.kt +++ b/app/src/main/java/org/sopt/and/component/MyPageItem.kt @@ -34,7 +34,7 @@ fun MyPageItem( .padding(bottom = 80.dp) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally - ){ + ) { Image( painter = icon, contentDescription = "", diff --git a/app/src/main/java/org/sopt/and/component/bar/BottomBar.kt b/app/src/main/java/org/sopt/and/component/bar/BottomBar.kt index 5613dbb..2277238 100644 --- a/app/src/main/java/org/sopt/and/component/bar/BottomBar.kt +++ b/app/src/main/java/org/sopt/and/component/bar/BottomBar.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController -import org.sopt.and.navigate.Routes import org.sopt.and.navigate.ScreenTab @Composable @@ -55,8 +54,8 @@ fun BottomBar( BottomTab( onClick = { - navController.navigate(tab.route){ - popUpTo(tab.route){ + navController.navigate(tab.route) { + popUpTo(tab.route) { inclusive = true } } diff --git a/app/src/main/java/org/sopt/and/component/bar/TopBar.kt b/app/src/main/java/org/sopt/and/component/bar/TopBar.kt index 8fdd774..026e0b8 100644 --- a/app/src/main/java/org/sopt/and/component/bar/TopBar.kt +++ b/app/src/main/java/org/sopt/and/component/bar/TopBar.kt @@ -27,7 +27,7 @@ fun TopBar(modifier: Modifier = Modifier) { modifier = Modifier.width(100.dp) ) - Row{ + Row { Image( painter = painterResource(R.drawable.outline_cast_connected_24), contentDescription = "", diff --git a/app/src/main/java/org/sopt/and/component/textField/IDTextfield.kt b/app/src/main/java/org/sopt/and/component/textField/IDTextfield.kt index 5922673..35d67e1 100644 --- a/app/src/main/java/org/sopt/and/component/textField/IDTextfield.kt +++ b/app/src/main/java/org/sopt/and/component/textField/IDTextfield.kt @@ -15,7 +15,7 @@ fun IDTextField( onValueChange: (String) -> Unit, placeholder: String, modifier: Modifier = Modifier -){ +) { TextField( value = value, onValueChange = onValueChange, diff --git a/app/src/main/java/org/sopt/and/navigate/NavGraph.kt b/app/src/main/java/org/sopt/and/navigate/NavGraph.kt index 3c982d0..0c4c5eb 100644 --- a/app/src/main/java/org/sopt/and/navigate/NavGraph.kt +++ b/app/src/main/java/org/sopt/and/navigate/NavGraph.kt @@ -16,8 +16,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import org.sopt.and.component.bar.BottomBar import org.sopt.and.ui.home.HomeScreen -import org.sopt.and.ui.home.HomeViewModel import org.sopt.and.ui.my.MyScreen +import org.sopt.and.ui.my.MyViewModel import org.sopt.and.ui.search.SearchScreen import org.sopt.and.ui.signin.SignInScreen import org.sopt.and.ui.signin.SignInViewModel @@ -25,9 +25,10 @@ import org.sopt.and.ui.signup.SignUpScreen import org.sopt.and.ui.signup.SignUpViewModel @Composable -fun NavGraph(navController: NavHostController){ +fun NavGraph(navController: NavHostController) { val signUpViewModel: SignUpViewModel = viewModel() val signInViewModel: SignInViewModel = viewModel() + val myViewModel: MyViewModel = viewModel() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route @@ -37,7 +38,12 @@ fun NavGraph(navController: NavHostController){ val selectedScreen = ScreenTab.entries.find { it.route == currentRoute } if (selectedScreen != null) { BottomBar(selected = selectedScreen, navController = navController) - }else if (currentRoute in listOf(Routes.Home.route, Routes.My.route, Routes.Search.route)) { + } else if (currentRoute in listOf( + Routes.Home.route, + Routes.My.route, + Routes.Search.route + ) + ) { BottomBar(selected = null, navController = navController) } } @@ -49,9 +55,24 @@ fun NavGraph(navController: NavHostController){ .padding(paddingValues) ) { NavHost(navController = navController, startDestination = Routes.SignIn.route) { - composable(Routes.SignIn.route) { SignInScreen(navController, signInViewModel = signInViewModel) } - composable(Routes.SignUp.route) { SignUpScreen(navController, signUpViewModel = signUpViewModel) } - composable(Routes.My.route) { MyScreen(navController, signInViewModel = signInViewModel) } + composable(Routes.SignIn.route) { + SignInScreen( + navController, + signInViewModel = signInViewModel + ) + } + composable(Routes.SignUp.route) { + SignUpScreen( + navController, + signUpViewModel = signUpViewModel + ) + } + composable(Routes.My.route) { + MyScreen( + navController, + myViewModel = myViewModel + ) + } composable(Routes.Search.route) { SearchScreen(navController) } composable(Routes.Home.route) { HomeScreen(navController) } } diff --git a/app/src/main/java/org/sopt/and/ui/my/MyScreen.kt b/app/src/main/java/org/sopt/and/ui/my/MyScreen.kt index 6975c51..001adc2 100644 --- a/app/src/main/java/org/sopt/and/ui/my/MyScreen.kt +++ b/app/src/main/java/org/sopt/and/ui/my/MyScreen.kt @@ -1,5 +1,6 @@ package org.sopt.and.ui.my +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -12,34 +13,45 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import org.sopt.and.R -import org.sopt.and.component.bar.BottomBar import org.sopt.and.component.BuyTextButton import org.sopt.and.component.MyPageItem -import org.sopt.and.ui.signin.SignInViewModel @Composable fun MyScreen( navController: NavHostController, - signInViewModel: SignInViewModel + myViewModel: MyViewModel ) { - val userId = signInViewModel.userInfo.userId + val context = LocalContext.current + val sharedPreferences = remember { + context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE) + } + + LaunchedEffect(Unit) { + myViewModel.getUserHobby(sharedPreferences) + } + + val hobby by myViewModel.hobby.collectAsState() Column( modifier = Modifier.fillMaxSize() - ){ + ) { Column( modifier = Modifier .background(Color.DarkGray) @@ -61,7 +73,7 @@ fun MyScreen( contentScale = ContentScale.Fit ) Text( - text = stringResource(R.string.my_nickname, userId), + text = stringResource(R.string.my_nickname, hobby), color = Color.White, modifier = Modifier.weight(1f) ) diff --git a/app/src/main/java/org/sopt/and/ui/my/MyViewModel.kt b/app/src/main/java/org/sopt/and/ui/my/MyViewModel.kt new file mode 100644 index 0000000..1c21a34 --- /dev/null +++ b/app/src/main/java/org/sopt/and/ui/my/MyViewModel.kt @@ -0,0 +1,59 @@ +package org.sopt.and.ui.my + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import okhttp3.ResponseBody +import org.sopt.and.api.ServicePool +import org.sopt.and.api.dto.ResponseErrorDto +import org.sopt.and.api.dto.ResponseHobbySuccessDto +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MyViewModel() : ViewModel() { + private val hobbyService = ServicePool.hobbyService + + private val _hobby = MutableStateFlow("") + val hobby: StateFlow get() = _hobby + + fun getUserHobby(sharedPreferences: SharedPreferences) { + val token = sharedPreferences.getString("token", null) + if (token.isNullOrEmpty()) { + Log.e("MyViewModel", "토큰 없음") + return + } + + hobbyService.getMyHobby(token).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val successBody = response.body()?.string() + val successDto = + Json.decodeFromString(successBody ?: "") + viewModelScope.launch { + _hobby.emit(successDto.result.hobby) + } + } else { + val errorBody = response.errorBody()?.string() + val errorDto = errorBody?.let { Json.decodeFromString(it) } + val errorMessage = when (response.code()) { + 401 -> "토큰이 없습니다." + 403 -> "유효하지 않은 토큰입니다." + 404 -> "잘못된 경로로 요청했습니다." + else -> "알 수 없는 오류가 발생했습니다." + } + Log.e("MyViewModel", errorMessage) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e("MyViewModel", "Network error: ${t.message}") + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/ui/search/SearchScreen.kt b/app/src/main/java/org/sopt/and/ui/search/SearchScreen.kt index 3a5fb74..4b03395 100644 --- a/app/src/main/java/org/sopt/and/ui/search/SearchScreen.kt +++ b/app/src/main/java/org/sopt/and/ui/search/SearchScreen.kt @@ -3,13 +3,10 @@ package org.sopt.and.ui.search import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.navigation.NavHostController -import org.sopt.and.component.bar.BottomBar @Composable fun SearchScreen( diff --git a/app/src/main/java/org/sopt/and/ui/signin/SignInScreen.kt b/app/src/main/java/org/sopt/and/ui/signin/SignInScreen.kt index ed24498..baca449 100644 --- a/app/src/main/java/org/sopt/and/ui/signin/SignInScreen.kt +++ b/app/src/main/java/org/sopt/and/ui/signin/SignInScreen.kt @@ -54,6 +54,7 @@ fun SignInScreen( val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } + signInViewModel.initializePreferences(context) LaunchedEffect(snackbarMessage) { @@ -104,8 +105,18 @@ fun SignInScreen( Button( onClick = { - signInViewModel.updateUserInfo(userId, userPassWord) - signInViewModel.performLogin() +// signInViewModel.updateUserInfo(userId, userPassWord) +// signInViewModel.performLogin() + signInViewModel.loginUser( + username = userId, + password = userPassWord, + onSuccess = { tokenMessage -> + signInViewModel._snackbarMessage.value = tokenMessage + }, + onFailure = { erorMessage -> + signInViewModel._snackbarMessage.value = erorMessage + } + ) }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors( diff --git a/app/src/main/java/org/sopt/and/ui/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/and/ui/signin/SignInViewModel.kt index b2ecaca..b9cd278 100644 --- a/app/src/main/java/org/sopt/and/ui/signin/SignInViewModel.kt +++ b/app/src/main/java/org/sopt/and/ui/signin/SignInViewModel.kt @@ -2,41 +2,77 @@ package org.sopt.and.ui.signin import android.content.Context import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import android.util.Log import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.sopt.and.model.UserInfo +import kotlinx.serialization.json.Json +import okhttp3.ResponseBody +import org.sopt.and.api.ServicePool +import org.sopt.and.api.dto.RequestLoginDto +import org.sopt.and.api.dto.ResponseErrorDto +import org.sopt.and.api.dto.ResponseLoginSuccessDto +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response class SignInViewModel : ViewModel() { - var userInfo by mutableStateOf(UserInfo("", "")) - var sharedPreferences: SharedPreferences? = null + val loginService by lazy { ServicePool.loginService } + private var sharedPreferences: SharedPreferences? = null - private val _snackbarMessage = MutableStateFlow(null) + val _snackbarMessage = MutableStateFlow(null) val snackbarMessage: StateFlow get() = _snackbarMessage fun initializePreferences(context: Context) { sharedPreferences = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE) } - fun updateUserInfo(id: String, password: String) { - userInfo = UserInfo(userId = id, userPassWord = password) + private fun saveToken(token: String) { + sharedPreferences?.edit()?.putString("token", token)?.apply() } - fun performLogin() { - val savedUserId = sharedPreferences?.getString("userId", "") ?: "" - val savedUserPassword = sharedPreferences?.getString("userPassWord", "") ?: "" - val loginSuccess = - (userInfo.userId == savedUserId && userInfo.userPassWord == savedUserPassword) - - _snackbarMessage.value = if (loginSuccess) { - "로그인 성공" - } else { - "로그인 실패" - } + fun loginUser( + username: String, + password: String, + onSuccess: (String) -> Unit, + onFailure: (String) -> Unit + ) { + val requestLoginDto = RequestLoginDto(username = username, password = password) + + loginService.postLogin(requestLoginDto).enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + val successBody = response.body()?.string() + val successDto = + Json.decodeFromString(successBody ?: "") + val token = successDto.result.token + saveToken(token) + onSuccess("로그인 성공!") + } else { + val errorBody = response.errorBody()?.string() + val errorDto = errorBody?.let { Json.decodeFromString(it) } + val errorMessage = when (response.code()) { + 400 -> when (errorDto?.code) { + "02" -> "로그인 정보가 올바르지 않습니다." + else -> "잘못된 요청입니다." + } + + 403 -> "비밀번호가 틀렸습니다." + else -> "알 수 없는 오류가 발생했습니다." + } + onFailure(errorMessage) + } + } + + override fun onFailure(call: Call, t: Throwable) { + onFailure("네트워크 오류: ${t.message}") + Log.e("SignInViewModel", "Failure: ${t.message}") + } + }) } fun clearSnackbarMessage() { diff --git a/app/src/main/java/org/sopt/and/ui/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/ui/signup/SignUpScreen.kt index 0d4e589..69876a1 100644 --- a/app/src/main/java/org/sopt/and/ui/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/ui/signup/SignUpScreen.kt @@ -43,12 +43,14 @@ fun SignUpScreen( var userPassWord by remember { mutableStateOf("") } + var userHobby by remember { + mutableStateOf("") + } val context = LocalContext.current - signUpViewModel.initializePreferences(context) Column( modifier = modifier - ){ + ) { Box( modifier = Modifier .fillMaxSize() @@ -58,7 +60,7 @@ fun SignUpScreen( modifier = Modifier .padding(20.dp) .align(Alignment.TopStart) - ){ + ) { Text( text = stringResource(R.string.signup_text), color = Color.Gray @@ -67,7 +69,7 @@ fun SignUpScreen( IDTextField( value = userId, - onValueChange = {userId = it}, + onValueChange = { userId = it }, modifier = Modifier.fillMaxWidth(), placeholder = context.getString(R.string.signup_id) ) @@ -88,6 +90,13 @@ fun SignUpScreen( InfoTextWithIcon( text = stringResource(R.string.signup_password_explain) ) + + IDTextField( + value = userHobby, + onValueChange = { userHobby = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = context.getString(R.string.signup_hobby) + ) } @@ -95,16 +104,26 @@ fun SignUpScreen( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - ){ + ) { Button( onClick = { - if (signUpViewModel.isAbleEmail(userId) && signUpViewModel.isAblePassword(userPassWord)){ - signUpViewModel.saveUserInfo(userId, userPassWord) - navController.popBackStack() - Toast.makeText(context, (R.string.signup_success),Toast.LENGTH_SHORT).show() - }else{ - Toast.makeText(context, (R.string.signup_fail),Toast.LENGTH_SHORT).show() - } + signUpViewModel.signUpUser( + username = userId, + password = userPassWord, + hobby = userHobby, + onSuccess = { + navController.popBackStack() + Toast.makeText( + context, + R.string.signup_success, + Toast.LENGTH_SHORT + ).show() + }, + onFailure = { errorMessage -> + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + } + ) + }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/sopt/and/ui/signup/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/ui/signup/SignUpViewModel.kt index a2728ef..0b29146 100644 --- a/app/src/main/java/org/sopt/and/ui/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/sopt/and/ui/signup/SignUpViewModel.kt @@ -1,39 +1,63 @@ package org.sopt.and.ui.signup -import android.content.Context -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import android.util.Log import androidx.lifecycle.ViewModel -import org.sopt.and.model.UserInfo - - -class SignUpViewModel: ViewModel() { - val PASSWORD_MIN_LENGTH = 8 - val PASSWORD_MAX_LENGTH = 20 - val PASSWORD_REGEX = Regex("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@\$!%*?&])[A-Za-z\\d@\$!%*?&]{$PASSWORD_MIN_LENGTH,$PASSWORD_MAX_LENGTH}\$") - - var sharedPreferences: SharedPreferences? = null - var userInfo by mutableStateOf(UserInfo("","")) - - fun initializePreferences(context: Context){ - sharedPreferences = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE) - } - - fun saveUserInfo(id: String, password: String){ - sharedPreferences?.edit()?.apply(){ - putString("userId", id) - putString("userPassWord", password) - apply() - } - } - - fun isAbleEmail(email: String): Boolean{ - return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() - } - - fun isAblePassword(password: String): Boolean{ - return PASSWORD_REGEX.matches(password) +import kotlinx.serialization.json.Json +import okhttp3.ResponseBody +import org.sopt.and.api.ServicePool +import org.sopt.and.api.dto.RequestUserDto +import org.sopt.and.api.dto.ResponseErrorDto +import org.sopt.and.api.dto.ResponseUserSuccessDto +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class SignUpViewModel : ViewModel() { + private val userService by lazy { ServicePool.userService } + + fun signUpUser( + username: String, + password: String, + hobby: String, + onSuccess: () -> Unit, + onFailure: (String) -> Unit + ) { + val requestUserDto = RequestUserDto(username = username, password = password, hobby = hobby) + + userService.postUser(requestUserDto).enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + val successBody = response.body()?.string() + val successDto = + Json.decodeFromString(successBody ?: "") + onSuccess() + } else { + val errorBody = response.errorBody()?.string() + val errorDto = errorBody?.let { Json.decodeFromString(it) } + val errorMessage = when (response.code()) { + 400 -> when (errorDto?.code) { + "01" -> "닉네임, 비밀번호, 취미가 8자를 넘기면 안됩니다." + else -> "잘못된 요청입니다." + } + + 409 -> when (errorDto?.code) { + "00" -> "닉네임이 중복됩니다." + else -> "충돌이 발생했습니다." + } + + else -> "알 수 없는 오류가 발생했습니다." + } + onFailure(errorMessage) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e("SignUpViewModel", "Failure: ${t.message}") + } + }) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28ab24a..b599df9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ wavve@example.com 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요. Wavve 비밀번호 설정 + 취미 설정 비밀번호는 8-20자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해 주세요. Wavve 회원가입 회원가입 성공 @@ -18,9 +19,10 @@ 비밀번호 재설정 회원가입 | - 로그인 성공 + 로그인 성공! 로그인 실패 + // My 화면 첫 결제 시 첫 달 100원! 현재 보유하신 이용권이 없습니다. @@ -42,7 +44,6 @@ 키즈 영화플러스 %1$d / %2$d - 믿고 보는 웨이브 에디터 추천작 오늘의 TOP 20 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c1df34..b0ade36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,10 @@ navigationCompose = "2.9.0-alpha01" coilCompose = "2.7.0" accompanistPager = "0.36.0" accompanistPagerIndicators = "0.36.0" +okhttp = "4.11.0" +retrofit = "2.9.0" +retrofitKotlinSerializationConverter = "1.0.0" +kotlinxSerializationJson = "1.6.3" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -34,8 +38,15 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanistPager" } accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanistPagerIndicators" } +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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }