diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36ed0bb..c7ef95b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +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) + id("kotlin-kapt") + id("com.google.dagger.hilt.android") +} + + +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) } android { @@ -16,6 +26,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) } buildTypes { @@ -36,6 +47,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -50,6 +62,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.runtime.ktx) + implementation(libs.androidx.runtime.livedata) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -60,5 +73,19 @@ dependencies { 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51166e1..a29a780 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + - - - + android:exported="true"> diff --git a/app/src/main/java/org/sopt/and/MainActivity.kt b/app/src/main/java/org/sopt/and/MainActivity.kt index 9a5cbc1..efd67f9 100644 --- a/app/src/main/java/org/sopt/and/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/MainActivity.kt @@ -1,51 +1,51 @@ package org.sopt.and +import android.app.Application import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.HiltAndroidApp import org.sopt.and.navigation.AuthNavItem import org.sopt.and.presentation.ui.auth.SignInScreen import org.sopt.and.presentation.ui.auth.SignUpScreen import org.sopt.and.presentation.ui.main.MainScreen -import org.sopt.and.presentation.viewmodel.SignUpViewModel import org.sopt.and.ui.theme.ANDANDROIDTheme +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { ANDANDROIDTheme { - val signUpViewModel: SignUpViewModel = viewModel() - MyApp(signUpViewModel) + MyAppScreen() } } } } +@HiltAndroidApp +class MyApp : Application() + @Composable -fun MyApp(signUpViewModel: SignUpViewModel) { +fun MyAppScreen() { val navController = rememberNavController() NavHost(navController, startDestination = AuthNavItem.SignUp.route) { composable(AuthNavItem.SignUp.route) { - SignUpScreen(signUpViewModel = signUpViewModel, navController = navController) + SignUpScreen(navController = navController) } - composable(AuthNavItem.SignIn.route) { - SignInScreen(signUpViewModel = signUpViewModel, navController = navController) + SignInScreen(navController = navController) } - composable(AuthNavItem.Main.route) { - MainScreen(signUpViewModel = signUpViewModel) + MainScreen() } - } - } diff --git a/app/src/main/java/org/sopt/and/data/api/ApiFactory.kt b/app/src/main/java/org/sopt/and/data/api/ApiFactory.kt new file mode 100644 index 0000000..adbe3e8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/api/ApiFactory.kt @@ -0,0 +1,60 @@ +package org.sopt.and.data.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 diff --git a/app/src/main/java/org/sopt/and/data/api/AuthService.kt b/app/src/main/java/org/sopt/and/data/api/AuthService.kt new file mode 100644 index 0000000..fb05d60 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/api/AuthService.kt @@ -0,0 +1,27 @@ +package org.sopt.and.data.api + +import org.sopt.and.data.dto.RequestLoginData +import org.sopt.and.data.dto.RequestUserRegistrationData +import org.sopt.and.data.dto.ResponseLogin +import org.sopt.and.data.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/data/api/HobbyService.kt b/app/src/main/java/org/sopt/and/data/api/HobbyService.kt new file mode 100644 index 0000000..303ca3b --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/api/HobbyService.kt @@ -0,0 +1,13 @@ +package org.sopt.and.data.api + +import org.sopt.and.data.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/data/dto/AuthDto.kt b/app/src/main/java/org/sopt/and/data/dto/AuthDto.kt new file mode 100644 index 0000000..e6286f5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/AuthDto.kt @@ -0,0 +1,48 @@ +package org.sopt.and.data.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/data/dto/MyviewDto.kt b/app/src/main/java/org/sopt/and/data/dto/MyviewDto.kt new file mode 100644 index 0000000..5f58918 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dto/MyviewDto.kt @@ -0,0 +1,17 @@ +package org.sopt.and.data.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/data/repository/Auth/LoginRepository.kt b/app/src/main/java/org/sopt/and/data/repository/Auth/LoginRepository.kt new file mode 100644 index 0000000..3b35b27 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repository/Auth/LoginRepository.kt @@ -0,0 +1,24 @@ +package org.sopt.and.data.repository.Auth + +import org.sopt.and.data.api.LoginService +import org.sopt.and.data.dto.RequestLoginData +import org.sopt.and.data.dto.ResponseLogin +import javax.inject.Inject + +class LoginRepository @Inject constructor( + private val apiService: LoginService +) { + suspend fun postLogin(requestData: RequestLoginData): Result { + return try { + val response = apiService.postLogin(requestData) + if (response.isSuccessful) { + Result.success(response.body()!!) + } else { + val errorCode = response.errorBody()?.string() ?: "알수없슴" + Result.failure(Exception("Error code : ${response.code()}, 에러 코드 : $errorCode")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repository/Auth/UserRegistrationRepository.kt b/app/src/main/java/org/sopt/and/data/repository/Auth/UserRegistrationRepository.kt new file mode 100644 index 0000000..7c41341 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repository/Auth/UserRegistrationRepository.kt @@ -0,0 +1,24 @@ +package org.sopt.and.data.repository.Auth + +import org.sopt.and.data.api.UserRegistrationService +import org.sopt.and.data.dto.RequestUserRegistrationData +import org.sopt.and.data.dto.ResponseUserRegistration +import javax.inject.Inject + +class UserRegistrationRepository @Inject constructor( + private val apiService: UserRegistrationService +) { + suspend fun postUserRegistration(requestData: RequestUserRegistrationData): Result { + return try { + val response = apiService.postUserRegistration(requestData) + if (response.isSuccessful) { + Result.success(response.body()!!) + } else { + val errorCode = response.errorBody()?.string() ?: "알수없슴" + Result.failure(Exception("Error code : ${response.code()}, 에러 코드 : $errorCode")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repository/MyviewRepository.kt b/app/src/main/java/org/sopt/and/data/repository/MyviewRepository.kt new file mode 100644 index 0000000..4965e11 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repository/MyviewRepository.kt @@ -0,0 +1,30 @@ +package org.sopt.and.data.repository + +import org.sopt.and.data.api.HobbyService +import javax.inject.Inject + +class MyviewRepository @Inject constructor( + private val apiService: HobbyService +) { + suspend fun getHobby( + token: String + ): Result { + return try { + val response = apiService.getHobby(token) + if (response.isSuccessful) { + val hobby = response.body()?.result?.hobby + if (hobby != null) { + Result.success(hobby) + } else { + Result.failure(Exception("Hobby data is null")) + } + } else { + val errorCode = response.code() + Result.failure(Exception("Error code : $errorCode")) + } + + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repository/SharedPreferencesHelper.kt b/app/src/main/java/org/sopt/and/data/repository/SharedPreferencesHelper.kt new file mode 100644 index 0000000..f32de53 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repository/SharedPreferencesHelper.kt @@ -0,0 +1,34 @@ +package org.sopt.and.data.repository + +import android.content.Context +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun provideSharedPreferencesHelper(@ApplicationContext context: Context): SharedPreferencesHelper { + return SharedPreferencesHelper(context) + } +} + + +class SharedPreferencesHelper(context: Context) { + private val prefs: SharedPreferences = + context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) + + fun saveToken(token: String) { + prefs.edit().putString("auth_token", token).apply() + } + + fun getToken(): String? { + return prefs.getString("auth_token", null) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/ui/auth/SignInScreen.kt b/app/src/main/java/org/sopt/and/presentation/ui/auth/SignInScreen.kt index 8e257c0..c7910b1 100644 --- a/app/src/main/java/org/sopt/and/presentation/ui/auth/SignInScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/ui/auth/SignInScreen.kt @@ -1,5 +1,6 @@ package org.sopt.and.presentation.ui.auth +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -15,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -27,7 +29,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import kotlinx.coroutines.launch @@ -38,23 +40,18 @@ import org.sopt.and.presentation.ui.auth.component.AuthTextField import org.sopt.and.presentation.ui.auth.component.ServiceIconRow import org.sopt.and.presentation.ui.auth.component.TextFieldValidateResult import org.sopt.and.presentation.viewmodel.SignInViewModel -import org.sopt.and.presentation.viewmodel.SignInViewModelFactory -import org.sopt.and.presentation.viewmodel.SignUpViewModel - @Composable -fun SignInScreen(signUpViewModel: SignUpViewModel, navController: NavHostController) { - - val factory = SignInViewModelFactory(signUpViewModel) - val signInViewModel: SignInViewModel = viewModel(factory = factory) +fun SignInScreen( + signInViewModel: SignInViewModel = hiltViewModel(), + navController: NavHostController +) { + val signInResult by signInViewModel.loginResult.observeAsState() var isPasswordVisible by remember { mutableStateOf(false) } - val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() - - Column( modifier = Modifier .background(color = Color.Black) @@ -94,14 +91,14 @@ fun SignInScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl ) { AuthTextField( - value = signInViewModel.emailLogin, - onValueChange = { signInViewModel.updateEmailLogin(it) }, + value = signInViewModel.username, + onValueChange = { signInViewModel.updateUsernameLogin(it) }, placeholder = "이메일 주소 또는 아이디", validateState = TextFieldValidateResult.Basic ) Spacer(modifier = Modifier.height(20.dp)) AuthTextField( - value = signInViewModel.passwordLogin, + value = signInViewModel.password, onValueChange = { signInViewModel.updatePasswordLogin(it) }, placeholder = "비밀번호", validateState = TextFieldValidateResult.Basic, @@ -123,32 +120,35 @@ fun SignInScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl AuthButton( text = "로그인", onClick = { - if (signInViewModel.validateSignIn() - ) { - coroutineScope.launch { - snackbarHostState.showSnackbar("로그인 성공!") - } - navController.navigate(AuthNavItem.Main.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - } - } else { - coroutineScope.launch { - snackbarHostState.showSnackbar("로그인 실패!") + signInViewModel.postLogin( + signInViewModel.username, + signInViewModel.password + ) + } + ) + signInResult?.let { result -> + if (result.isSuccess) { + coroutineScope.launch { + snackbarHostState.showSnackbar("로그인 성공!") + } + navController.navigate(AuthNavItem.Main.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true } + launchSingleTop = true } + } else { + coroutineScope.launch { + snackbarHostState.showSnackbar("로그인 실패!") + } + val errorMessage = result.exceptionOrNull()?.message ?: "로그인 실패!!" + Log.e("signInScreen", errorMessage) } - ) - + } AuthServiceDescription() ServiceIconRow() - } } - - } } diff --git a/app/src/main/java/org/sopt/and/presentation/ui/auth/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/ui/auth/SignUpScreen.kt index 5787777..45046ae 100644 --- a/app/src/main/java/org/sopt/and/presentation/ui/auth/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/ui/auth/SignUpScreen.kt @@ -1,5 +1,6 @@ package org.sopt.and.presentation.ui.auth +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -8,9 +9,15 @@ 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.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -25,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import org.sopt.and.R import org.sopt.and.navigation.AuthNavItem @@ -37,8 +45,13 @@ import org.sopt.and.presentation.viewmodel.SignUpViewModel @Composable -fun SignUpScreen(signUpViewModel: SignUpViewModel, navController: NavHostController) { +fun SignUpScreen( + signUpViewModel: SignUpViewModel = hiltViewModel(), + navController: NavHostController +) { + val signUpResult by signUpViewModel.userRegistrationResult.observeAsState() + val context = LocalContext.current Column( modifier = Modifier @@ -60,16 +73,14 @@ fun SignUpScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl fontSize = 20.sp, textAlign = TextAlign.Center ) - Text( - text = "❌", + Icon( + Icons.Default.Close, + tint = Color.White, + contentDescription = "close", modifier = Modifier - .align(Alignment.CenterEnd), - fontSize = 15.sp, - color = Color.White + .align(Alignment.CenterEnd) ) } - - val context = LocalContext.current Column( modifier = Modifier .background(Color.Black) @@ -89,18 +100,19 @@ fun SignUpScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl modifier = Modifier.padding(start = 15.dp, top = 30.dp, end = 15.dp), text = text, fontSize = 28.sp, + lineHeight = 40.sp, color = Color.White, ) - Spacer(modifier = Modifier.height(30.dp)) + Spacer(modifier = Modifier.height(15.dp)) AuthTextField( modifier = Modifier .fillMaxWidth() .padding(start = 15.dp, top = 30.dp, end = 15.dp), - value = signUpViewModel.email, - onValueChange = { signUpViewModel.email = it }, + value = signUpViewModel.username, + onValueChange = { signUpViewModel.username = it }, placeholder = "wavve@example.com", validateState = TextFieldValidateResult.Basic, - infoDescription = stringResource(R.string.signup_email_description) + infoDescription = stringResource(R.string.signup_username_description) ) Spacer(modifier = Modifier.height(10.dp)) AuthTextField( @@ -128,22 +140,35 @@ fun SignUpScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl }, infoDescription = stringResource(R.string.signup_password_description) ) + Spacer(modifier = Modifier.height(10.dp)) + AuthTextField( + modifier = Modifier + .fillMaxWidth() + .padding(start = 15.dp, top = 30.dp, end = 15.dp), + value = signUpViewModel.hobby, + onValueChange = { signUpViewModel.hobby = it }, + placeholder = "ex) 음악 감상", + validateState = TextFieldValidateResult.Basic, + infoDescription = stringResource(R.string.signup_hobby_description) + ) + Spacer(modifier = Modifier.height(40.dp)) AuthServiceDescription() ServiceIconRow() Spacer(modifier = Modifier.weight(1f)) TextButton( onClick = { if (signUpViewModel.validateSignUp( - signUpViewModel.email, - signUpViewModel.password + signUpViewModel.username, + signUpViewModel.password, + signUpViewModel.hobby ) ) { - signUpViewModel.setEmailAndPassword( - signUpViewModel.email, - signUpViewModel.password + signUpViewModel.registerUser( + signUpViewModel.username, + signUpViewModel.password, + signUpViewModel.hobby ) - navController.navigate(AuthNavItem.SignIn.route) } else { context.showToast(context, "회원가입 조건에 부합하지 않습니다.") } @@ -161,6 +186,19 @@ fun SignUpScreen(signUpViewModel: SignUpViewModel, navController: NavHostControl ) } + signUpResult?.let { result -> + if (result.isSuccess) { + LaunchedEffect(Unit) { + context.showToast(context, "회원가입 성공!") + navController.navigate(AuthNavItem.SignIn.route) + } + } else { + val errorMessage = result.exceptionOrNull()?.message ?: "회원가입 실패" + Log.e("signupscreen", errorMessage) + context.showToast(context, errorMessage) + } + } + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/ui/main/BottomNavGraph.kt b/app/src/main/java/org/sopt/and/presentation/ui/main/BottomNavGraph.kt index 6ab2c07..77cc48d 100644 --- a/app/src/main/java/org/sopt/and/presentation/ui/main/BottomNavGraph.kt +++ b/app/src/main/java/org/sopt/and/presentation/ui/main/BottomNavGraph.kt @@ -5,10 +5,9 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import org.sopt.and.navigation.BottomNavItem -import org.sopt.and.presentation.viewmodel.SignUpViewModel @Composable -fun BottomNavGraph(navController: NavHostController, signUpViewModel: SignUpViewModel) { +fun BottomNavGraph(navController: NavHostController) { NavHost(navController = navController, startDestination = BottomNavItem.Home.route) { composable(route = BottomNavItem.Home.route) { HomeScreen() @@ -17,7 +16,7 @@ fun BottomNavGraph(navController: NavHostController, signUpViewModel: SignUpView SearchScreen() } composable(route = BottomNavItem.MY.route) { - MyviewScreen(signUpViewModel = signUpViewModel) + MyviewScreen() } } diff --git a/app/src/main/java/org/sopt/and/presentation/ui/main/MainScreen.kt b/app/src/main/java/org/sopt/and/presentation/ui/main/MainScreen.kt index edc079d..e0a2ecd 100644 --- a/app/src/main/java/org/sopt/and/presentation/ui/main/MainScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/ui/main/MainScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination @@ -24,14 +25,16 @@ import org.sopt.and.navigation.BottomNavItem import org.sopt.and.presentation.viewmodel.SignUpViewModel @Composable -fun MainScreen(signUpViewModel: SignUpViewModel) { +fun MainScreen( + signUpViewModel: SignUpViewModel = hiltViewModel() +) { val navController = rememberNavController() Scaffold( bottomBar = { BottomBar(navController = navController) } ) { Box(Modifier.padding(it)) { - BottomNavGraph(navController = navController, signUpViewModel = signUpViewModel) + BottomNavGraph(navController = navController) } } } diff --git a/app/src/main/java/org/sopt/and/presentation/ui/main/MyviewScreen.kt b/app/src/main/java/org/sopt/and/presentation/ui/main/MyviewScreen.kt index f776c2a..97eb49e 100644 --- a/app/src/main/java/org/sopt/and/presentation/ui/main/MyviewScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/ui/main/MyviewScreen.kt @@ -13,19 +13,27 @@ import androidx.compose.material.icons.filled.AccountCircle 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.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import org.sopt.and.R import org.sopt.and.presentation.ui.main.component.MyPurchaseInfoCol -import org.sopt.and.presentation.viewmodel.SignUpViewModel +import org.sopt.and.presentation.viewmodel.MyviewViewModel @Composable -fun MyviewScreen(signUpViewModel: SignUpViewModel) { +fun MyviewScreen( + myviewViewModel: MyviewViewModel = hiltViewModel() +) { + val hobbyResult by myviewViewModel.hobbyResult.observeAsState() + Column( modifier = Modifier @@ -44,14 +52,35 @@ fun MyviewScreen(signUpViewModel: SignUpViewModel) { .size(60.dp) .align(Alignment.CenterStart) ) - Text( - text = signUpViewModel.email, - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = 85.dp), - fontSize = 10.sp, - color = Color.White - ) + + LaunchedEffect(Unit) { + myviewViewModel.getHobby() + } + + hobbyResult?.let { result -> + if (result.isSuccess) { + result.getOrNull()?.let { + Text( + text = it, + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 85.dp), + fontSize = 10.sp, + color = Color.White + ) + } + } else { + Text( + text = "취미 가져오는데 실패..", + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 85.dp), + fontSize = 10.sp, + color = Color.White + ) + } + } + Text( text = "🔔", diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodel/MyviewViewModel.kt b/app/src/main/java/org/sopt/and/presentation/viewmodel/MyviewViewModel.kt new file mode 100644 index 0000000..a106665 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/viewmodel/MyviewViewModel.kt @@ -0,0 +1,36 @@ +package org.sopt.and.presentation.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.data.repository.MyviewRepository +import org.sopt.and.data.repository.SharedPreferencesHelper +import javax.inject.Inject + +@HiltViewModel +class MyviewViewModel @Inject constructor( + private val sharedPreferencesHelper: SharedPreferencesHelper, + private val myviewRepository: MyviewRepository +) : ViewModel() { + private val _hobbyResult = MutableLiveData>() + val hobbyResult: LiveData> get() = _hobbyResult + + fun getHobby() { + viewModelScope.launch { + val token = getTokenForRequest() + if (token != null) { + val result = myviewRepository.getHobby(token) + _hobbyResult.postValue(result) + } else { + _hobbyResult.postValue(Result.failure(Exception("토큰이 없습니다."))) + } + } + } + + fun getTokenForRequest(): String? { + return sharedPreferencesHelper.getToken() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/viewmodel/SignInViewModel.kt index 7c08ee5..9663434 100644 --- a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignInViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/viewmodel/SignInViewModel.kt @@ -1,55 +1,61 @@ package org.sopt.and.presentation.viewmodel +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -class SignInViewModel( - private val signUpViewModel: SignUpViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.data.dto.RequestLoginData +import org.sopt.and.data.dto.ResponseLogin +import org.sopt.and.data.repository.Auth.LoginRepository +import org.sopt.and.data.repository.SharedPreferencesHelper +import javax.inject.Inject + +@HiltViewModel +class SignInViewModel @Inject constructor( + private val loginRepository: LoginRepository, + private val sharedPreferencesHelper: SharedPreferencesHelper ) : ViewModel() { - private var _emailLogin by mutableStateOf("") - val emailLogin: String - get() = _emailLogin - - private var _passwordLogin by mutableStateOf("") - val passwordLogin: String - get() = _passwordLogin - - var emailError by mutableStateOf("") - var passwordError by mutableStateOf("") - - fun updateEmailLogin(newEmail: String) { - _emailLogin = newEmail - validateEmail() + private val _loginResult = MutableLiveData>() + val loginResult: LiveData> get() = _loginResult + + fun postLogin(username: String, password: String) { + viewModelScope.launch { + val loginRequest = RequestLoginData(username, password) + val result = loginRepository.postLogin(loginRequest) + result.onSuccess { + val token = it.result.token + saveToken(token) + Log.d("SignInResultViewModel", "토큰이 저장됐을까.. $token") + } + _loginResult.postValue(result) + } } - fun updatePasswordLogin(newPassword: String) { - _passwordLogin = newPassword - validatePassword() + fun saveToken(token: String) { + sharedPreferencesHelper.saveToken(token) } - fun validateEmail() { - emailError = if (emailLogin.isEmpty()) "이메일을 입력하세요." else "" - } + private var _username by mutableStateOf("") + val username: String + get() = _username - fun validatePassword() { - passwordError = if (passwordLogin.isEmpty()) "비밀번호를 입력하세요." else "" + private var _password by mutableStateOf("") + val password: String + get() = _password + + fun updateUsernameLogin(username: String) { + _username = username } - fun validateSignIn(): Boolean = - emailLogin == signUpViewModel.email && passwordLogin == signUpViewModel.password + fun updatePasswordLogin(password: String) { + _password = password + } } - -class SignInViewModelFactory(private val signUpViewmodel: SignUpViewModel) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(SignInViewModel::class.java)) { - return SignInViewModel(signUpViewmodel) as T - } - throw IllegalArgumentException("알 수 없는 뷰 모델 클래스") - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/viewmodel/SignUpViewModel.kt index 073e8f3..e880422 100644 --- a/app/src/main/java/org/sopt/and/presentation/viewmodel/SignUpViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/viewmodel/SignUpViewModel.kt @@ -3,38 +3,40 @@ package org.sopt.and.presentation.viewmodel import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import org.sopt.and.presentation.utils.Constants -import org.sopt.and.presentation.utils.RegexConstants - -class SignUpViewModel : ViewModel() { +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.data.dto.RequestUserRegistrationData +import org.sopt.and.data.dto.ResponseUserRegistration +import org.sopt.and.data.repository.Auth.UserRegistrationRepository +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val userRegistrationRepository: UserRegistrationRepository +) : ViewModel() { + + private val _userRegistrationResult = MutableLiveData>() + val userRegistrationResult: LiveData> get() = _userRegistrationResult + + fun registerUser(username: String, password: String, hobby: String) { + viewModelScope.launch { + val userRequest = RequestUserRegistrationData(username, password, hobby) + val result = userRegistrationRepository.postUserRegistration(userRequest) + _userRegistrationResult.postValue(result) + } + } - var email by mutableStateOf("") + var username by mutableStateOf("") var password by mutableStateOf("") + var hobby by mutableStateOf("") var isPasswordVisible by mutableStateOf(false) - - fun validateSignUp(email: String, password: String): Boolean { - if (!RegexConstants.EMAIL_REGEX.matches(email)) { - return false - } - if (password.length > Constants.MAX_PASSWORD_LENGTH || password.length < Constants.MIN_PASSWORD_LENGTH) { - return false - } - val hasLowerCase = RegexConstants.LOWER_CASE_REGEX.containsMatchIn(password) - val hasUpperCase = RegexConstants.UPPER_CASE_REGEX.containsMatchIn(password) - val hasDigitCase = RegexConstants.DIGIT_REGEX.containsMatchIn(password) - val hasSpecialChar = RegexConstants.SPECIAL_REGEX.containsMatchIn(password) - - val criteriaCnt = - listOf(hasLowerCase, hasUpperCase, hasDigitCase, hasSpecialChar).count { it } - - return criteriaCnt >= Constants.PASSWORD_CRITERIA_COUNT - } - - fun setEmailAndPassword(email: String, password: String) { - this.email = email - this.password = password + fun validateSignUp(username: String, password: String, hobby: String): Boolean { + return username.length <= 8 && password.length <= 8 && hobby.length <= 8 } +} -} \ 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 f8d0cba..30963e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,8 +10,9 @@ 믿고 보는 웨이브 에디터 추천작 오늘의 TOP 20 - 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요. - 비밀번호는 8~20자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해주세요. + 8자 이하의 이름을 입력해주세요. + 8자 이하의 비밀번호를 입력해주세요. + 8자 이하의 취미를 입력해주세요. 또는 다른 서비스 계정으로 가입 첫 결제 시 첫 달 100원! diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..8952fb3 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 f0d715a..3ff8a15 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,9 @@ [versions] 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.13.1" junit = "4.13.2" @@ -12,12 +16,24 @@ 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" } @@ -32,9 +48,17 @@ 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-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" } 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" }