diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61fde81..ec19294 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -5,18 +7,22 @@ plugins { alias(libs.plugins.kotlin.serialization) } +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) +} + android { namespace = "org.sopt.and" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "org.sopt.and" minSdk = 28 - targetSdk = 34 + targetSdk = 35 versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) } buildTypes { @@ -37,6 +43,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -65,4 +72,10 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + 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 c5ca09d..bb70edb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + - - - - - - - - - - - - - + () - val email: LiveData = _email - - fun setEmail(newEmail: String) { - _email.value = newEmail - } -} +import org.sopt.and.data.datalocal.datasourceimpl.UserInfoLocalDataSourceImpl +import org.sopt.and.presentation.loginScreen.LoginScreen +import org.sopt.and.presentation.mypageScreen.MypageScreen +import org.sopt.and.presentation.searchScreen.SearchScreen +import org.sopt.and.presentation.signupScreen.SignUpScreen +import org.sopt.and.presentation.homeScreen.HomeScreen +import org.sopt.and.presentation.homeScreen.HomeViewModel +import org.sopt.and.presentation.loginScreen.LoginViewModel +import org.sopt.and.presentation.mypageScreen.MypageViewModel +import org.sopt.and.util.Route class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,60 +29,70 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { ANDANDROIDTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - val navController = rememberNavController() - val userViewModel: UserViewModel = viewModel() + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { it + val navController = rememberNavController() - NavHost( - navController = navController, - startDestination = SignUpScreen, - modifier = Modifier.padding(innerPadding) - ){ - composable { - SignUpScreen( - navigateToLoginScreen = { - emailText, passwordText -> navController.navigate(LoginScreen(emailText, passwordText)) - } - ) - } + val context = navController.context + val userInfoLocalDataSource = UserInfoLocalDataSourceImpl(context) - composable { backStackEntry -> - val item = backStackEntry.toRoute() - val scope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } - LoginScreen( - emailText = item.emailText, - passwordText = item.passwordText, - scope = scope, - snackbarHostState = snackbarHostState, - navigateToHomeScreen = { - navController.navigate("home") - } - ) - } + NavHost( + navController = navController, + startDestination = Route.SignUpScreen(userName = "", password = ""), + modifier = Modifier + ){ + composable { backStackEntry -> + val item = backStackEntry.toRoute() + SignUpScreen( + navigateToLoginScreen = { + navController.navigate(Route.LoginScreen){ + popUpTo { inclusive = true } + launchSingleTop = true + } + } + ) + } - composable("home") { - HomeScreen( - navController = navController, - ) - } + composable { backStackEntry -> + val item = backStackEntry.toRoute() + LoginScreen( + loginViewModel = LoginViewModel(userInfoLocalDataSource = userInfoLocalDataSource), + navigateToHomeScreen = { + navController.navigate(Route.HomeScreen){ + popUpTo { inclusive = true} + launchSingleTop = true + } + }, + ) + } - composable("search") { - SearchScreen( - navController = navController - ) - } + composable { backStackEntry -> + val item = backStackEntry.toRoute() + HomeScreen( + homeViewModel = HomeViewModel(), + navController = navController, + ) + } - composable("profile") { + composable { backStackEntry -> + val item = backStackEntry.toRoute() + SearchScreen( + navController = navController + ) + } - MypageScreen( - navController = navController, - userViewModel = userViewModel - ) + composable { backStackEntry -> + val item = backStackEntry.toRoute() + MypageScreen( + mypageViewModel = MypageViewModel(userInfoLocalDataSource = userInfoLocalDataSource), + navController = navController + ) + } } - } - } + + }) } } } diff --git a/app/src/main/java/org/sopt/and/data/datalocal/datasource/UserInfoLocalDataSource.kt b/app/src/main/java/org/sopt/and/data/datalocal/datasource/UserInfoLocalDataSource.kt new file mode 100644 index 0000000..c3d939b --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/datalocal/datasource/UserInfoLocalDataSource.kt @@ -0,0 +1,7 @@ +package org.sopt.and.data.datalocal.datasource + +interface UserInfoLocalDataSource { + var accessToken: String + var userName: String + fun clear() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt b/app/src/main/java/org/sopt/and/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt new file mode 100644 index 0000000..f4124a9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt @@ -0,0 +1,29 @@ +package org.sopt.and.data.datalocal.datasourceimpl + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource + +class UserInfoLocalDataSourceImpl(context: Context) : UserInfoLocalDataSource { + + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + + override var accessToken: String + get() = sharedPreferences.getString(ACCESSTOKEN, INITIAL_VALUE).toString() + set(value) = sharedPreferences.edit { putString(ACCESSTOKEN, value)} + + override var userName: String + get() = sharedPreferences.getString(USERNAME, INITIAL_VALUE).toString() + set(value) = sharedPreferences.edit { putString(USERNAME, value) } + + override fun clear() = sharedPreferences.edit { clear() } + + companion object { + const val PREFERENCES_NAME = "user_preferences" + const val ACCESSTOKEN = "accesstoken" + const val USERNAME = "userName" + const val INITIAL_VALUE = "" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/dto/login/RequestGetUserDto.kt b/app/src/main/java/org/sopt/and/data/dto/login/RequestGetUserDto.kt new file mode 100644 index 0000000..6af7ed9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/login/RequestGetUserDto.kt @@ -0,0 +1,12 @@ +package org.sopt.and.data.dto.login + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestGetUserDto( + @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/data/dto/login/ResponseGetUserDto.kt b/app/src/main/java/org/sopt/and/data/dto/login/ResponseGetUserDto.kt new file mode 100644 index 0000000..01855a2 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/login/ResponseGetUserDto.kt @@ -0,0 +1,22 @@ +package org.sopt.and.data.dto.login + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetUserFailedDto( + @SerialName("code") + val code: String +) + +@Serializable +data class ResponseGetUserDto( + @SerialName("result") + val result: UserResult +){ + @Serializable + data class UserResult( + @SerialName("token") + val token: String + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/dto/mypage/ResponseGetUserHobbySuccessDto.kt b/app/src/main/java/org/sopt/and/data/dto/mypage/ResponseGetUserHobbySuccessDto.kt new file mode 100644 index 0000000..efb25e7 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/mypage/ResponseGetUserHobbySuccessDto.kt @@ -0,0 +1,23 @@ +package org.sopt.and.data.dto.mypage + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseGetUserHobbyFailDto( + @SerialName("code") + val no: Int +) + +@Serializable +data class ResponseGetUserHobbyDto( + @SerialName("result") + val result: Result?= null, +){ + @Serializable + data class Result( + @SerialName("hobby") + val userHobby: String? = null, + ) +} + diff --git a/app/src/main/java/org/sopt/and/data/dto/signup/RequestCreateUserDto.kt b/app/src/main/java/org/sopt/and/data/dto/signup/RequestCreateUserDto.kt new file mode 100644 index 0000000..cff38ae --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/signup/RequestCreateUserDto.kt @@ -0,0 +1,14 @@ +package org.sopt.and.data.dto.signup + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestCreateUserDto( + @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/data/dto/signup/ResponseCreateUserDto.kt b/app/src/main/java/org/sopt/and/data/dto/signup/ResponseCreateUserDto.kt new file mode 100644 index 0000000..69b7301 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/signup/ResponseCreateUserDto.kt @@ -0,0 +1,23 @@ +package org.sopt.and.data.dto.signup + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseCreateUserFailedDto( + @SerialName("code") + val code: String +) + +@Serializable +data class ResponseCreateUserSuccessDto( + @SerialName("result") + val result: UserResult +){ + @Serializable + data class UserResult( + @SerialName("no") + val no: Int + ) +} + diff --git a/app/src/main/java/org/sopt/and/data/network/ApiFactory.kt b/app/src/main/java/org/sopt/and/data/network/ApiFactory.kt new file mode 100644 index 0000000..9e7ad20 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/network/ApiFactory.kt @@ -0,0 +1,37 @@ +package org.sopt.and.data.network + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +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 + +object ApiFactory { + private const val BASE_URL: String = BuildConfig.BASE_URL + + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + inline fun create(): T = retrofit.create(T::class.java) +} + +//이 userService를, 서버를 붙이는 부분에서 사용해야 함. +//Viewmodel에서 이 userService에 접근하면 됨. +object ServicePool { + val userService = ApiFactory.create() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/network/UserService.kt b/app/src/main/java/org/sopt/and/data/network/UserService.kt new file mode 100644 index 0000000..8991ce8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/network/UserService.kt @@ -0,0 +1,36 @@ +package org.sopt.and.data.network + +import org.sopt.and.data.dto.login.RequestGetUserDto +import org.sopt.and.data.dto.login.ResponseGetUserDto +import org.sopt.and.data.dto.mypage.ResponseGetUserHobbyDto +import org.sopt.and.data.dto.signup.RequestCreateUserDto +import org.sopt.and.data.dto.signup.ResponseCreateUserSuccessDto +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +//HTTP 메서드를 정의해둔 인터페이스 == service라고 부름. + +interface UserService { + + @POST("/user") + suspend fun signUpUser( + @Body requestDto: RequestCreateUserDto + ): Response + + @POST("/login") + suspend fun logInUser( + @Body request: RequestGetUserDto + ): Response + + @GET("/user/my-hobby") + suspend fun getMyHobby( + @Header("token") token: String + ): Response + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/User.kt b/app/src/main/java/org/sopt/and/domain/User.kt new file mode 100644 index 0000000..7d05423 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/User.kt @@ -0,0 +1,8 @@ +package org.sopt.and.domain + +data class User( + var name: String = "", + var password: String = "", + var hobby: String = "", + var accessToken: String = "", +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/HomeActivity.kt b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt similarity index 64% rename from app/src/main/java/org/sopt/and/HomeActivity.kt rename to app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt index c004ba0..4c4d989 100644 --- a/app/src/main/java/org/sopt/and/HomeActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt @@ -1,10 +1,8 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +package org.sopt.and.presentation.homeScreen -package org.sopt.and - -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -16,7 +14,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,32 +24,34 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable +import org.sopt.and.presentation.mypageScreen.MypageScreen +import org.sopt.and.presentation.searchScreen.SearchScreen import org.sopt.and.ui.components.BottomBar.CustomBottomAppBar import org.sopt.and.ui.components.HomeScreen.HomeLazyRow import org.sopt.and.ui.components.TopBar.CustomTopAppBar import org.sopt.and.ui.components.TopBar.CustomTopAppBarSecond import org.sopt.and.ui.theme.ANDANDROIDTheme -@Serializable -data object HomeScreen -@OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( modifier: Modifier = Modifier, - navController: NavController, // navController를 넘겨 받아 사용 + navController: NavController, + homeViewModel: HomeViewModel = viewModel() ) { val context = LocalContext.current val scrollState = rememberScrollState() Scaffold( topBar = { - Column { + Column( + modifier = modifier.fillMaxWidth() + ){ CustomTopAppBar(navController = navController) CustomTopAppBarSecond(navController = navController) } @@ -60,25 +59,16 @@ fun HomeScreen( bottomBar = { CustomBottomAppBar(navController = navController) } - ) { innerPadding -> + ) { it Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) .background(Color(0xFF1B1B1B)) - .padding(innerPadding) // 패딩 적용 .padding(all = 10.dp) ) { - val images = listOf( - R.drawable.food_pic1, - R.drawable.food_pic2, - R.drawable.food_pic3, - R.drawable.food_pic4, - R.drawable.food_pic5 - ) - - val pagerState = rememberPagerState { images.size } + val pagerState = rememberPagerState { homeViewModel.mainPagerImages.size } HorizontalPager( state = pagerState, @@ -91,7 +81,7 @@ fun HomeScreen( .fillMaxSize() .padding(16.dp) .clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = images[idx]), + painter = painterResource(id = homeViewModel.mainPagerImages[idx]), contentDescription = "imagePager", contentScale = ContentScale.Crop ) @@ -99,7 +89,7 @@ fun HomeScreen( HomeLazyRow( title = "믿고 보는 웨이브 에디터 추천작", - images = images, + images = homeViewModel.mainPagerImages, height = 230, width = 140, ) @@ -107,7 +97,7 @@ fun HomeScreen( HomeLazyRow( title = "실시간 인기 콘텐츠", - images = images, + images = homeViewModel.mainPagerImages, height = 230, width = 140, ) @@ -115,7 +105,7 @@ fun HomeScreen( HomeLazyRow( title = "오직 웨이브에서", - images = images, + images = homeViewModel.mainPagerImages, height = 230, width = 140, ) @@ -123,7 +113,7 @@ fun HomeScreen( HomeLazyRow( title = "오늘의 TOP 20", - images = images, + images = homeViewModel.mainPagerImages, height = 260, width = 180, ) @@ -131,7 +121,7 @@ fun HomeScreen( HomeLazyRow( title = "당한 대로 갚아줄게", - images = images, + images = homeViewModel.mainPagerImages, height = 230, width = 140, ) @@ -146,45 +136,11 @@ fun HomeScreen( @Composable fun HomeScreenPreview() { val navController = rememberNavController() + val homeViewModel = HomeViewModel() - ANDANDROIDTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Column{ - CustomTopAppBar(navController = navController) - CustomTopAppBarSecond(navController = navController) - - } + HomeScreen( + navController = navController, + homeViewModel = homeViewModel + ) - }, - bottomBar = { - CustomBottomAppBar(navController = navController) - } - ) { - innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFF1B1B1B)) - .padding(innerPadding) - ){ - NavHost( - navController = navController, - startDestination = "home", - ){ - composable("home") {HomeScreen( - navController = navController - )} - composable("search") {SearchScreen( - navController = navController - )} - composable("profile") {MypageScreen( - navController = navController, - )} - } - } - - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeViewModel.kt b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeViewModel.kt new file mode 100644 index 0000000..93b63ad --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeViewModel.kt @@ -0,0 +1,17 @@ +package org.sopt.and.presentation.homeScreen + +import androidx.compose.foundation.pager.PagerState +import androidx.lifecycle.ViewModel +import org.sopt.and.R + +class HomeViewModel : ViewModel() { + + val mainPagerImages = listOf( + R.drawable.food_pic1, + R.drawable.food_pic2, + R.drawable.food_pic3, + R.drawable.food_pic4, + R.drawable.food_pic5 + ) + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/LoginActivity.kt b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt similarity index 57% rename from app/src/main/java/org/sopt/and/LoginActivity.kt rename to app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt index ca074c4..884ca61 100644 --- a/app/src/main/java/org/sopt/and/LoginActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt @@ -1,6 +1,5 @@ -package org.sopt.and +package org.sopt.and.presentation.loginScreen -import androidx.activity.ComponentActivity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -20,20 +19,29 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import org.sopt.and.presentation.signupScreen.StringInputValidCheck +import org.sopt.and.presentation.signupScreen.PasswordValidCheck +import org.sopt.and.R +import org.sopt.and.data.dto.login.RequestGetUserDto +import org.sopt.and.data.dto.signup.RequestCreateUserDto +import org.sopt.and.presentation.main.UserViewModel +import org.sopt.and.presentation.mypageScreen.MypageViewModel import org.sopt.and.ui.components.SignUpandLogIn.SignUpTextField import org.sopt.and.ui.components.SignUpandLogIn.SocialLoginSection import org.sopt.and.ui.theme.ANDANDROIDTheme @@ -41,36 +49,32 @@ import org.sopt.and.ui.theme.ANDANDROIDTheme @Serializable data class LoginScreen( - val emailText: String, + val userNameText: String, val passwordText: String ) @Composable fun LoginScreen( modifier: Modifier = Modifier.fillMaxSize(), - scope: CoroutineScope, - snackbarHostState: SnackbarHostState, - emailText: String, - passwordText: String, navigateToHomeScreen: () -> Unit, - userViewModel: UserViewModel = viewModel() + loginViewModel: LoginViewModel, ) { - var inputEmail: String = "" - var inputPassword: String = "" + var userNameText = remember { mutableStateOf("") } + var passwordText = remember { mutableStateOf("") } - var emailState = remember { mutableStateOf(inputEmail) } - var passwordState = remember { mutableStateOf(inputPassword) } + var isUserNameValid = loginViewModel.isUserNameValid.collectAsState().value + var isPasswordValid = loginViewModel.isPasswordValid.collectAsState().value + var shouldShowPassword = loginViewModel.shouldShowPassword.collectAsState().value + val loginResult = loginViewModel.loginResult.collectAsState().value - var isEmailValid = remember { mutableStateOf(true) } - var isPasswordValid = remember { mutableStateOf(true) } - - var shouldShowPassword = remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val snackBarHostState = remember {SnackbarHostState()} Scaffold( modifier = modifier, snackbarHost = { - SnackbarHost(hostState = snackbarHostState) + SnackbarHost(hostState = snackBarHostState) } ) { innerPadding -> Column( @@ -92,35 +96,37 @@ fun LoginScreen( ) } - // Email 입력 필드 + // UserName 입력 필드 SignUpTextField( - text = emailState.value, - onValueChange = { newValue -> - emailState.value = newValue - isEmailValid.value = EmailValidCheck(emailState.value) + text = userNameText.value, + onValueChange = { + userNameText.value = it + loginViewModel.onUserNameChange(it) + isUserNameValid = StringInputValidCheck(userNameText.value) }, - fieldType = "Email", - conditionCheck = isEmailValid.value, - errMessage = "올바른 이메일 형식이 아닙니다.", - placeholder = "wavve@example.com", + fieldType = "UserName", + conditionCheck = isUserNameValid, + errMessage = "유저 이름은 7자 이하여야 합니다.", + placeholder = "유저 이름 (7자 이하)", ) Spacer(modifier = Modifier.weight(0.025f)) // Password 입력 필드 SignUpTextField( - text = passwordState.value, - onValueChange = { newValue -> - passwordState.value = newValue - isPasswordValid.value = PasswordValidCheck(passwordState.value) + text = passwordText.value, + onValueChange = { + passwordText.value = it + loginViewModel.onPasswordChange(it) + isPasswordValid = PasswordValidCheck(passwordText.value) }, fieldType = "Password", - conditionCheck = isPasswordValid.value, + conditionCheck = isPasswordValid, errMessage = "올바른 비밀번호 형식이 아닙니다.", placeholder = "Wavve 비밀번호 설정", - shouldShowPassword = shouldShowPassword.value, + shouldShowPassword = shouldShowPassword, onPasswordVisibilityChange = { - shouldShowPassword.value = !shouldShowPassword.value + loginViewModel.togglePasswordVisibility() }, ) @@ -129,22 +135,10 @@ fun LoginScreen( // 로그인 버튼 Button( onClick = { - var loginMessage = "" - var loginSuccessFlag = 0 - - if (emailState.value == emailText && passwordState.value == passwordText) { - loginMessage = "로그인 성공" - loginSuccessFlag = 1 - } else { - loginMessage = "알맞은 이메일과 비밀번호를 입력하세요" - } - - scope.launch { - val snackbarResult = snackbarHostState.showSnackbar(loginMessage) - - if (loginSuccessFlag == 1 && snackbarResult == SnackbarResult.Dismissed) { - userViewModel.setEmail(emailState.value) - navigateToHomeScreen() + // 일단 글자 조건(8자)에 맞으면 logInUser API를 불러 봄 + if (loginViewModel.isLoginValid(userNameText.value, passwordText.value)) { + coroutineScope.launch { + loginViewModel.logInUser() } } }, @@ -173,23 +167,29 @@ fun LoginScreen( SocialLoginSection(modifier = modifier) Spacer(modifier = Modifier.weight(1f)) } + LaunchedEffect(loginResult){ + loginResult?.let { + if (loginResult == true) { + snackBarHostState.showSnackbar(message = "로그인에 성공했습니다.") + delay(300) + navigateToHomeScreen() + } else { + snackBarHostState.showSnackbar(message = "유저 이름 혹은 비밀번호를 확인하세요.") + } + } + } } } -@Preview(showBackground = true) -@Composable -fun LoginScreenPreview2() { - ANDANDROIDTheme { - val scope = rememberCoroutineScope() - val snackbarHostState = remember { SnackbarHostState() } - LoginScreen( - scope = scope, - snackbarHostState = snackbarHostState, - emailText = "", - passwordText = "", - navigateToHomeScreen = {}, - ) - } -} \ No newline at end of file +//@Preview(showBackground = true) +//@Composable +//fun LoginScreenPreview2() { +// ANDANDROIDTheme { +// val scope = rememberCoroutineScope() +// val snackbarHostState = remember { SnackbarHostState() } +// LoginScreen( +// ) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginViewModel.kt b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginViewModel.kt new file mode 100644 index 0000000..9de63ba --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginViewModel.kt @@ -0,0 +1,104 @@ +package org.sopt.and.presentation.loginScreen + +import android.util.Log +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.json.Json +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.domain.User +import org.sopt.and.data.dto.login.RequestGetUserDto +import org.sopt.and.data.dto.login.ResponseGetUserFailedDto +import org.sopt.and.data.dto.signup.RequestCreateUserDto +import org.sopt.and.data.dto.signup.ResponseCreateUserFailedDto +import org.sopt.and.data.network.ServicePool +import org.sopt.and.presentation.main.UserViewModel +import org.sopt.and.presentation.mypageScreen.MypageViewModel +import org.sopt.and.presentation.signupScreen.StringInputValidCheck +import org.sopt.and.presentation.signupScreen.PasswordValidCheck +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class LoginViewModel( + private val userInfoLocalDataSource: UserInfoLocalDataSource +) : ViewModel() { + + //로그인 요청을 서버로 보내기 위함 + private val userService by lazy { ServicePool.userService } + + private val _user = MutableStateFlow(User()) + val user: StateFlow = _user + + private val _loginResult = MutableStateFlow(null) + val loginResult = _loginResult.asStateFlow() + + private val _isUserNameValid = MutableStateFlow(false) + val isUserNameValid: StateFlow = _isUserNameValid + + private val _isPasswordValid = MutableStateFlow(false) + val isPasswordValid: StateFlow = _isPasswordValid + + private val _shouldShowPassword = MutableStateFlow(false) + val shouldShowPassword: StateFlow = _shouldShowPassword + + //유저네임 입력 시 입력한 글자 표시 + fun onUserNameChange(newUserName: String) { + _user.value = _user.value.copy(name = newUserName) + _isUserNameValid.value = StringInputValidCheck(newUserName) + } + + //비밀번호 입력 시 입력한 글자 표시 + fun onPasswordChange(newPassword: String) { + _user.value = _user.value.copy(password = newPassword) + _isPasswordValid.value = PasswordValidCheck(newPassword) + } + + // 비밀번호 표시 여부 바꾸기 + fun togglePasswordVisibility() { + _shouldShowPassword.value = !_shouldShowPassword.value + } + + // 입력값이 조건에 맞는지 확인 (둘 다 여덟 자 이하?) + fun isLoginValid(userNameText: String, passwordText: String): Boolean { + return _isUserNameValid.value && _isPasswordValid.value + } + + suspend fun logInUser() { + val requestDto = RequestGetUserDto( + userName = _user.value.name, + password = _user.value.password + ) + + try { + val response = userService.logInUser(requestDto) + val token = response.body()?.result?.token + + if(response.isSuccessful && token != null) { + userInfoLocalDataSource.accessToken = token + userInfoLocalDataSource.userName = _user.value.name + _loginResult.value = true + Log.d( + "로그인 요청 성공", + "Status code: ${response.code()} token: $token" + ) + } else { + val errorBody = response.errorBody()?.string() + Log.e("서버 응답", "Raw response body: $errorBody") + val errorCode = if (errorBody != null) { + val errorData = Json.decodeFromString(errorBody) + errorData.code + } else { + "Unknown error code" + } + Log.e("error", "Status code: ${response.code()} and error code: $errorCode") + _loginResult.value = false + } + } catch (e: Exception) { + Log.e("error", "Exception: ${e.message}") + _loginResult.value = false + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/main/UserViewModel.kt b/app/src/main/java/org/sopt/and/presentation/main/UserViewModel.kt new file mode 100644 index 0000000..a9eb535 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/main/UserViewModel.kt @@ -0,0 +1,33 @@ +package org.sopt.and.presentation.main + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.sopt.and.presentation.signupScreen.StringInputValidCheck + +class UserViewModel : ViewModel() { + + private val _userName = MutableStateFlow("") + val userName: StateFlow = _userName + + private val _hobby = MutableStateFlow("") + val hobby: StateFlow = _hobby + + private val _loginToken = MutableStateFlow("") + val loginToken: StateFlow = _loginToken + + fun setUserName(newUserName: String) { + _userName.value = newUserName + } + + fun setHobby(newHobby: String) { + _hobby.value = newHobby + } + + fun setLoginToken(newToken: String) { + _loginToken.value = newToken + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/MyActivity.kt b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt similarity index 61% rename from app/src/main/java/org/sopt/and/MyActivity.kt rename to app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt index 3790d39..00582be 100644 --- a/app/src/main/java/org/sopt/and/MyActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt @@ -1,82 +1,39 @@ -package org.sopt.and +package org.sopt.and.presentation.mypageScreen -import android.content.Intent -import android.graphics.Paint -import android.graphics.drawable.Icon -import android.os.Bundle -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material.icons.outlined.Notifications -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable import org.sopt.and.ui.components.BottomBar.CustomBottomAppBar -import org.sopt.and.ui.components.BottomBar.NavIcon import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection2 import org.sopt.and.ui.components.MypageScreen.MyPageSubSection -import org.sopt.and.ui.components.TopBar.CustomTopAppBar import org.sopt.and.ui.theme.ANDANDROIDTheme import androidx.compose.runtime.livedata.observeAsState +import org.sopt.and.presentation.main.UserViewModel @Composable fun MypageScreen( navController: NavController, - userViewModel: UserViewModel = viewModel() + mypageViewModel: MypageViewModel = viewModel() ) { - - val context = LocalContext.current - val emailText = userViewModel.email.observeAsState("").value + val user by mypageViewModel.user.collectAsState() Scaffold( bottomBar = { @@ -90,7 +47,8 @@ fun MypageScreen( .padding(innerPadding) ) { MyPageProfileSection( - deliveredEmail = emailText + deliveredUserName = user.name, + deliveredUserHobby = user.hobby ) Spacer(modifier = Modifier.height(0.5.dp)) MyPageProfileSection2( diff --git a/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageViewModel.kt b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageViewModel.kt new file mode 100644 index 0000000..7fb2579 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageViewModel.kt @@ -0,0 +1,53 @@ +package org.sopt.and.presentation.mypageScreen + +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 org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.data.network.ServicePool +import org.sopt.and.domain.User + +class MypageViewModel( + private val userInfoLocalDataSource: UserInfoLocalDataSource +) : ViewModel() { + + private val userService by lazy { ServicePool.userService } + + private val _user = MutableStateFlow(User()) + val user: StateFlow = _user + + init { + loadUserData() + } + + fun loadUserData() { + viewModelScope.launch { + val userName = userInfoLocalDataSource.userName + val accessToken = userInfoLocalDataSource.accessToken + val hobby = getUserHobby() + _user.value.name = userName + _user.value.hobby = hobby + _user.value.accessToken = accessToken + } + } + + suspend fun getUserHobby(): String { + try { + val response = userService.getMyHobby(userInfoLocalDataSource.accessToken) + if (response.isSuccessful) { + Log.d("취미 조회 API 성공", "status code: ${response.code()}") + return response.body()?.result?.userHobby ?: "hobby" + } else { + Log.d("에러", "status code: ${response.code()}") + return "취미 가져오기 에러" + } + } catch (e: Exception) { + Log.e("error", "Exception: ${e.message}") + return "취미 가져오기 에러" + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/SearchActivity.kt b/app/src/main/java/org/sopt/and/presentation/searchScreen/SearchScreen.kt similarity index 96% rename from app/src/main/java/org/sopt/and/SearchActivity.kt rename to app/src/main/java/org/sopt/and/presentation/searchScreen/SearchScreen.kt index 367814f..7e6bb06 100644 --- a/app/src/main/java/org/sopt/and/SearchActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/searchScreen/SearchScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and +package org.sopt.and.presentation.searchScreen import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/org/sopt/and/SignUpActivity.kt b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt similarity index 53% rename from app/src/main/java/org/sopt/and/SignUpActivity.kt rename to app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt index 166e5f1..d4916cf 100644 --- a/app/src/main/java/org/sopt/and/SignUpActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt @@ -1,28 +1,20 @@ -package org.sopt.and +package org.sopt.and.presentation.signupScreen -import android.content.Intent -import android.os.Bundle -import android.util.Patterns import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -30,49 +22,57 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import org.sopt.and.domain.User +import org.sopt.and.data.dto.signup.RequestCreateUserDto +import org.sopt.and.presentation.main.UserViewModel import org.sopt.and.ui.components.SignUpandLogIn.SignUpTextField import org.sopt.and.ui.components.SignUpandLogIn.SocialLoginSection import org.sopt.and.ui.theme.ANDANDROIDTheme +import kotlin.math.sign @Serializable data object SignUpScreen -fun EmailValidCheck(email: String): Boolean { +fun StringInputValidCheck(newString: String): Boolean { var isValid = false - val inputStr : CharSequence = email - val pattern = Patterns.EMAIL_ADDRESS - val matcher = pattern.matcher(inputStr) - if(matcher.matches()){ - isValid = true + val inputStr : CharSequence = newString + + if(inputStr.length >= 8){ + return isValid + } else { + return !isValid } - return isValid } fun PasswordValidCheck(password: String): Boolean { - val pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#\$%^&*])|(?=.*[a-z])(?=.*\\d)(?=.*[!@#\$%^&*])|(?=.*[A-Z])(?=.*\\d)(?=.*[!@#\$%^&*]).{8,20}\$".toRegex() + val pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)|(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#\$%^&*])|(?=.*[a-z])(?=.*\\d)(?=.*[!@#\$%^&*])|(?=.*[A-Z])(?=.*\\d)(?=.*[!@#\$%^&*]).{1,8}\$".toRegex() return password.matches(pattern) - } + @Composable fun SignUpScreen( modifier: Modifier = Modifier, - navigateToLoginScreen: (emailText: String, passwordText: String) -> Unit, + navigateToLoginScreen: (user: User) -> Unit = {}, + signUpViewModel: SignUpViewModel = viewModel() ) { val context = LocalContext.current - - var emailFlag = 0 - var passwordFlag = 0 //8~20자 이내 조건 확인 var toastMessage = "" - var emailText = remember { mutableStateOf("") } + var userNameText = remember { mutableStateOf("") } var passwordText = remember { mutableStateOf("") } + var hobbyText = remember { mutableStateOf("") } + + var isUserNameValid = signUpViewModel.isUserNameValid.collectAsState().value + var isPasswordValid = signUpViewModel.isPasswordValid.collectAsState().value + var isHobbyValid = signUpViewModel.isHobbyValid.collectAsState().value + var shouldShowPassword = signUpViewModel.shouldShowPassword.collectAsState().value - var shouldShowPassword = remember {mutableStateOf(false)} - var isEmailValid = remember { mutableStateOf(true) } - var isPasswordValid = remember { mutableStateOf(true) } + val coroutineScope = rememberCoroutineScope() Column( modifier = Modifier @@ -92,7 +92,7 @@ fun SignUpScreen( Spacer(modifier = Modifier.weight(0.35f)) Text( - "이메일과 비밀번호만으로\nWavve를 즐길 수 있어요!", + "유저 이름, 비밀번호, 취미 입력만으로\nWavve를 즐길 수 있어요!", color = Color.White, fontSize = 21.sp ) @@ -100,37 +100,52 @@ fun SignUpScreen( Spacer(modifier = Modifier.weight(0.25f)) SignUpTextField( - text = emailText.value, - onValueChange = { newValue -> - emailText.value = newValue - isEmailValid.value = EmailValidCheck(emailText.value) + text = userNameText.value, + onValueChange = { + userNameText.value = it + signUpViewModel.onUserNameChange(it) + isUserNameValid = StringInputValidCheck(it) }, - fieldType = "Email", - conditionCheck = isEmailValid.value, - errMessage = "올바른 이메일 형식이 아닙니다.", - placeholder = "wavve@example.com", - descriptionText = "로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요.", + fieldType = "userName", + conditionCheck = isUserNameValid, + errMessage = "유저 이름은 7자 이하여야 합니다.", + placeholder = "유저 이름 (7자 이하)", + descriptionText = "로그인, 비밀번호 찾기, 알림에 사용되니 정확하게 입력해주세요.", ) Spacer(modifier = Modifier.weight(0.15f)) SignUpTextField( text = passwordText.value, - onValueChange = { newValue -> - passwordText.value = newValue - isPasswordValid.value = PasswordValidCheck(passwordText.value) + onValueChange = { + passwordText.value = it + signUpViewModel.onPasswordChange(it) /*todo: onpasswordchange 안에 isvalid 체킹하는 로직을 넣기.*/ + isPasswordValid = PasswordValidCheck(it) }, fieldType = "Password", - conditionCheck = isPasswordValid.value, + conditionCheck = isPasswordValid, errMessage = "올바른 비밀번호 형식이 아닙니다.", placeholder = "Wavve 비밀번호 설정", - shouldShowPassword = shouldShowPassword.value, + shouldShowPassword = shouldShowPassword, onPasswordVisibilityChange = { - shouldShowPassword.value = !shouldShowPassword.value + signUpViewModel.togglePasswordVisibility() }, descriptionText = "비밀번호는 8~20자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해 주세요.", ) + SignUpTextField( + text = hobbyText.value, + onValueChange = { + hobbyText.value = it + signUpViewModel.onHobbyChange(it) + isHobbyValid = StringInputValidCheck(it) + }, + fieldType = "hobby", + conditionCheck = isHobbyValid, + errMessage = "취미은 7자 이하여야 합니다.", + placeholder = "취미 입력", + ) + Spacer(modifier = Modifier.weight(0.5f)) SocialLoginSection(modifier = modifier) Spacer(modifier = Modifier.weight(1f)) @@ -144,26 +159,27 @@ fun SignUpScreen( .padding(vertical = 13.dp) .clickable { - //이메일 형식 조건 검사 - if (!EmailValidCheck(emailText.value)) { - emailFlag = 1 - toastMessage = "형식에 맞는 이메일을 입력하세요" + /*Todo: 클릭 시 조건 검사 및 토스트 띄우는 것까지 createNewUser 안에 넣기*/ - } + //유저 네임 형식 조건 검사 + if (!StringInputValidCheck(userNameText.value)) { + toastMessage = "형식에 맞는 유저 네임을 입력하세요" + } //비밀번호 형식 조건 검사 if (!PasswordValidCheck(passwordText.value)) { - passwordFlag = 1 toastMessage = "조건에 맞는 비밀번호를 사용하세요" } - if (emailFlag == 0 && passwordFlag == 0) { + if (signUpViewModel.isSignUpValid()) { + + coroutineScope.launch { + signUpViewModel.createNewUser() + } - toastMessage = "로그인 되었습니다" + toastMessage = "회원가입에 성공하였습니다." - //전달해줄 인자를 이 안에 넣으면 되는 듯.. - navigateToLoginScreen(emailText.value, passwordText.value) - println("네비게이트는 지남...") + navigateToLoginScreen(User(userNameText.value, passwordText.value)) } Toast @@ -183,10 +199,6 @@ fun SignUpScreen( fun SignUpPreview() { ANDANDROIDTheme { SignUpScreen( - navigateToLoginScreen = { - email, password -> - println("email: $email, password: $password") - } ) } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpViewModel.kt new file mode 100644 index 0000000..f757962 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpViewModel.kt @@ -0,0 +1,99 @@ +package org.sopt.and.presentation.signupScreen + +import android.util.Log +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.json.Json +import org.sopt.and.domain.User +import org.sopt.and.data.dto.signup.RequestCreateUserDto +import org.sopt.and.data.dto.signup.ResponseCreateUserFailedDto +import org.sopt.and.data.dto.signup.ResponseCreateUserSuccessDto +import org.sopt.and.data.network.ServicePool +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class SignUpViewModel : ViewModel() { + + //회원가입 성공 시 서버로 create 요청 보내기 위함 + private val userService by lazy { ServicePool.userService } + + private val _user = MutableStateFlow(User()) + val user: StateFlow = _user + + private val _signUpResult = MutableStateFlow?>(null) + val signUpResult: StateFlow?> = _signUpResult + + + private val _isUserNameValid = MutableStateFlow(true) + val isUserNameValid: StateFlow = _isUserNameValid + + private val _isPasswordValid = MutableStateFlow(true) + val isPasswordValid: StateFlow = _isPasswordValid + + private val _isHobbyValid = MutableStateFlow(true) + val isHobbyValid: StateFlow = _isHobbyValid + + private val _shouldShowPassword = MutableStateFlow(false) + val shouldShowPassword: StateFlow = _shouldShowPassword + + + // 유저 네임 입력 시 입력한 값 보이기 + fun onUserNameChange(newUserName: String) { + _user.value = _user.value.copy(name = newUserName) + _isUserNameValid.value = StringInputValidCheck(newUserName) + } + + // 비밀번호 입력 시 입력한 값 보이기 + fun onPasswordChange(newPassword: String) { + _user.value = _user.value.copy(password = newPassword) + _isPasswordValid.value = PasswordValidCheck(newPassword) + } + + // 취미 입력 시 입력한 값 보이기 + fun onHobbyChange(newHobby: String) { + _user.value = _user.value.copy(hobby = newHobby) + _isHobbyValid.value = StringInputValidCheck(newHobby) + } + + // 비밀번호 노출 상태 변경 시 + fun togglePasswordVisibility() { + _shouldShowPassword.value = !_shouldShowPassword.value + } + + fun isSignUpValid(): Boolean { + return _isUserNameValid.value && _isPasswordValid.value + } + + suspend fun createNewUser() { + val requestDto = RequestCreateUserDto( + userName = _user.value.name, + password = _user.value.password, + hobby = _user.value.hobby + ) + + try { + val response = userService.signUpUser(requestDto) + if(response.isSuccessful) { + _signUpResult.value = Result.success(Unit) + Log.d("로그인 성공", "Status code: ${response.code()}") + } else { + val errorBody = response.errorBody()?.string() + Log.e("서버 응답", "Raw response body: $errorBody") + val errorCode = if (errorBody != null){ + val errorData = Json.decodeFromString(errorBody) + errorData.code + } else { + "Unknown error code" + } + + _signUpResult.value = Result.failure(Exception("Status code is ${response.code()} and error code is $errorCode")) + } + } catch (e: Exception) { + Log.e("Login error", "Exception: ${e.message}") + _signUpResult.value = Result.failure(e) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/ui/components/BottomBar/CustomBottomAppBar.kt b/app/src/main/java/org/sopt/and/ui/components/BottomBar/CustomBottomAppBar.kt index 93db214..8324c5c 100644 --- a/app/src/main/java/org/sopt/and/ui/components/BottomBar/CustomBottomAppBar.kt +++ b/app/src/main/java/org/sopt/and/ui/components/BottomBar/CustomBottomAppBar.kt @@ -18,7 +18,6 @@ fun CustomBottomAppBar(navController: NavController) { BottomAppBar( containerColor = Color.Black, contentColor = Color.White, - ) { Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavIcon.kt b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavIcon.kt index d3a8206..35bb9e3 100644 --- a/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavIcon.kt +++ b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavIcon.kt @@ -16,6 +16,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import org.sopt.and.R +import org.sopt.and.presentation.homeScreen.HomeScreen +import org.sopt.and.presentation.homeScreen.HomeViewModel +import org.sopt.and.presentation.mypageScreen.MypageScreen +import org.sopt.and.presentation.mypageScreen.MypageViewModel +import org.sopt.and.presentation.searchScreen.SearchScreen +import org.sopt.and.util.Route @Composable fun NavIcon( @@ -26,7 +32,19 @@ fun NavIcon( text: String ){ Column( - modifier = modifier.clickable {navController.navigate(route)}, + modifier = modifier.clickable { + when (route) { + "home" -> { + navController.navigate(Route.HomeScreen) + } + "search" -> { + navController.navigate(Route.SearchScreen) + } + "profile" -> { + navController.navigate(Route.MypageScreen(userName = "")) /*username을 여기다 어떻게 넣어주지?*/ + } + } + }, horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally ){ if(text != "MY"){ diff --git a/app/src/main/java/org/sopt/and/ui/components/MypageScreen/MyPageProfileSection.kt b/app/src/main/java/org/sopt/and/ui/components/MypageScreen/MyPageProfileSection.kt index 1fc8269..9d8ceb6 100644 --- a/app/src/main/java/org/sopt/and/ui/components/MypageScreen/MyPageProfileSection.kt +++ b/app/src/main/java/org/sopt/and/ui/components/MypageScreen/MyPageProfileSection.kt @@ -27,9 +27,9 @@ import org.sopt.and.R @Composable fun MyPageProfileSection( - deliveredEmail: String, + deliveredUserName: String, + deliveredUserHobby: String?, ){ - Column( modifier = Modifier .fillMaxWidth() @@ -52,7 +52,7 @@ fun MyPageProfileSection( ) Spacer(modifier = Modifier.width(10.dp)) Text( - "${deliveredEmail}님", + "${deliveredUserHobby}를 즐기는\n${deliveredUserName}님", color = Color.White ) Spacer(modifier = Modifier.weight(0.5f)) diff --git a/app/src/main/java/org/sopt/and/ui/components/SignUpandLogIn/SignUpTextField.kt b/app/src/main/java/org/sopt/and/ui/components/SignUpandLogIn/SignUpTextField.kt index 70bdb5e..ab62a3d 100644 --- a/app/src/main/java/org/sopt/and/ui/components/SignUpandLogIn/SignUpTextField.kt +++ b/app/src/main/java/org/sopt/and/ui/components/SignUpandLogIn/SignUpTextField.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.unit.sp fun SignUpTextField( modifier: Modifier = Modifier, onValueChange: (String) -> Unit, - fieldType: String, //Email 혹은 Password로 전달 예정 + fieldType: String, //Username 혹은 Password로 전달 예정 text: String, conditionCheck: Boolean, errMessage: String, diff --git a/app/src/main/java/org/sopt/and/ui/components/TopBar/CustopTopAppBar.kt b/app/src/main/java/org/sopt/and/ui/components/TopBar/CustopTopAppBar.kt index 95ff0a3..7db3b3d 100644 --- a/app/src/main/java/org/sopt/and/ui/components/TopBar/CustopTopAppBar.kt +++ b/app/src/main/java/org/sopt/and/ui/components/TopBar/CustopTopAppBar.kt @@ -17,8 +17,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController import org.sopt.and.R @Composable @@ -55,4 +57,11 @@ fun CustomTopAppBar(navController: NavController){ ) } ) +} + +@Preview(showBackground = true) +@Composable +fun AppbarFirstPreivew(){ + val navController = rememberNavController() + CustomTopAppBar(navController = navController) } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/util/Constraints.kt b/app/src/main/java/org/sopt/and/util/Constraints.kt new file mode 100644 index 0000000..bc5a882 --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/Constraints.kt @@ -0,0 +1,26 @@ +package org.sopt.and.util + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Route { + @Serializable + data object HomeScreen : Route() + + @Serializable + data class SignUpScreen( + val userName: String, + val password: String + ) : Route() + + @Serializable + data object LoginScreen : Route() + + @Serializable + data class MypageScreen( + val userName: String + ) : Route() + + @Serializable + data object SearchScreen : Route() +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 20e2a01..132244e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7da403f..b33b409 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,9 @@ activityCompose = "1.8.0" composeBom = "2024.04.01" androidxComposeNavigation = "2.8.2" kotlinxSerializationJson = "1.7.3" +okhttp = "4.11.0" +retrofit = "2.9.0" +retrofitKotlinSerializationConverter = "1.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -27,6 +30,11 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation"} +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]