diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61fde81..959efd9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,22 +1,30 @@ +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) + alias(libs.plugins.dagger.hilt) + id("org.jetbrains.kotlin.kapt") +} + +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 +45,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } @@ -65,4 +74,17 @@ 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) + + implementation(libs.bundles.hilt) + kapt(libs.hilt.compiler) +} + +hilt { + enableAggregatingTask = false } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5ca09d..fe1293f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + - - - - - - - - - - - - - + Unit, - userViewModel: UserViewModel = viewModel() -) { - - var inputEmail: String = "" - var inputPassword: String = "" - - var emailState = remember { mutableStateOf(inputEmail) } - var passwordState = remember { mutableStateOf(inputPassword) } - - var isEmailValid = remember { mutableStateOf(true) } - var isPasswordValid = remember { mutableStateOf(true) } - - var shouldShowPassword = remember { mutableStateOf(false) } - - Scaffold( - modifier = modifier, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFF1B1B1B)) - .padding(innerPadding) - .padding(25.dp) - ) { - Spacer(modifier = Modifier.height(10.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Image( - painter = painterResource(id = R.drawable.wavve_logo), - contentDescription = "Wavve Logo", - modifier = Modifier.size(100.dp) - ) - } - - // Email 입력 필드 - SignUpTextField( - text = emailState.value, - onValueChange = { newValue -> - emailState.value = newValue - isEmailValid.value = EmailValidCheck(emailState.value) - }, - fieldType = "Email", - conditionCheck = isEmailValid.value, - errMessage = "올바른 이메일 형식이 아닙니다.", - placeholder = "wavve@example.com", - ) - - Spacer(modifier = Modifier.weight(0.025f)) - - // Password 입력 필드 - SignUpTextField( - text = passwordState.value, - onValueChange = { newValue -> - passwordState.value = newValue - isPasswordValid.value = PasswordValidCheck(passwordState.value) - }, - fieldType = "Password", - conditionCheck = isPasswordValid.value, - errMessage = "올바른 비밀번호 형식이 아닙니다.", - placeholder = "Wavve 비밀번호 설정", - shouldShowPassword = shouldShowPassword.value, - onPasswordVisibilityChange = { - shouldShowPassword.value = !shouldShowPassword.value - }, - ) - - Spacer(modifier = Modifier.weight(0.2f)) - - // 로그인 버튼 - 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() - } - } - }, - colors = ButtonDefaults.buttonColors(containerColor = Color.Blue), - modifier = Modifier.fillMaxWidth() - ) { - Text("로그인", color = Color.White, modifier = Modifier.padding(vertical = 8.dp)) - } - - Spacer(modifier = Modifier.weight(0.2f)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Text("아이디 찾기", color = Color.Gray, fontSize = 13.sp) - Text(" | ", color = Color.Gray, fontSize = 13.sp) - Text("비밀번호 재설정", color = Color.Gray, fontSize = 13.sp) - Text(" | ", color = Color.Gray, fontSize = 13.sp) - Text("회원가입", color = Color.Gray, fontSize = 13.sp) - } - - Spacer(modifier = Modifier.weight(0.2f)) - - // 소셜 로그인 섹션 - SocialLoginSection(modifier = modifier) - Spacer(modifier = Modifier.weight(1f)) - } - } -} - - - -@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 diff --git a/app/src/main/java/org/sopt/and/MainActivity.kt b/app/src/main/java/org/sopt/and/MainActivity.kt index e1463d5..d341bb7 100644 --- a/app/src/main/java/org/sopt/and/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/MainActivity.kt @@ -1,98 +1,100 @@ 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.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import org.sopt.and.ui.theme.ANDANDROIDTheme import androidx.navigation.compose.composable import androidx.navigation.toRoute +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.HiltAndroidApp +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 -//로그인 성공 시 로그인한 이메일을 viewmodel에 담아 전역변수로 관리.. -class UserViewModel : ViewModel() { - - private val _email = MutableLiveData() - val email: LiveData = _email - - fun setEmail(newEmail: String) { - _email.value = newEmail - } -} - +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 - 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( + 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( + navController = navController + ) + } } - } - } + + }) } } } } + diff --git a/app/src/main/java/org/sopt/and/MyApp.kt b/app/src/main/java/org/sopt/and/MyApp.kt new file mode 100644 index 0000000..9670811 --- /dev/null +++ b/app/src/main/java/org/sopt/and/MyApp.kt @@ -0,0 +1,8 @@ +package org.sopt.and + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + + +@HiltAndroidApp +class MyApp : Application() \ No newline at end of file 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..e209706 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt @@ -0,0 +1,32 @@ +package org.sopt.and.data.datalocal.datasourceimpl + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import javax.inject.Inject + +class UserInfoLocalDataSourceImpl @Inject constructor( + @ApplicationContext private val 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/dataremote/datasource/UserInfoRemoteDataSource.kt b/app/src/main/java/org/sopt/and/data/dataremote/datasource/UserInfoRemoteDataSource.kt new file mode 100644 index 0000000..79d7aa5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/datasource/UserInfoRemoteDataSource.kt @@ -0,0 +1,17 @@ +package org.sopt.and.data.dataremote.datasource + +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyFailDto +import retrofit2.Response + +interface UserInfoRemoteDataSource { + suspend fun postSignup(requestCreateUserDto: RequestCreateUserDto): Response + + suspend fun postLogin(requestGetUserDto: RequestGetUserDto): Response + + suspend fun getUserHobby(token: String): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/dataremote/datasourceimpl/UserInfoRemoteDateSourceImpl.kt b/app/src/main/java/org/sopt/and/data/dataremote/datasourceimpl/UserInfoRemoteDateSourceImpl.kt new file mode 100644 index 0000000..7e58521 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/datasourceimpl/UserInfoRemoteDateSourceImpl.kt @@ -0,0 +1,24 @@ +package org.sopt.and.data.dataremote.datasourceimpl + +import org.sopt.and.data.dataremote.datasource.UserInfoRemoteDataSource +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.data.dataremote.network.UserService +import retrofit2.Response +import javax.inject.Inject + +class UserInfoRemoteDataSourceImpl @Inject constructor ( + private val service: UserService +) : UserInfoRemoteDataSource { + override suspend fun postSignup(requestCreateUserDto: RequestCreateUserDto): Response = + service.signUpUser(requestCreateUserDto) + + override suspend fun postLogin(requestGetUserDto: RequestGetUserDto): Response = + service.logInUser(requestGetUserDto) + + override suspend fun getUserHobby(token: String): Response = + service.getMyHobby(token) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/dataremote/model/request/RequestCreateUserDto.kt b/app/src/main/java/org/sopt/and/data/dataremote/model/request/RequestCreateUserDto.kt new file mode 100644 index 0000000..0940f8e --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/request/RequestCreateUserDto.kt @@ -0,0 +1,14 @@ +package org.sopt.and.data.dataremote.model.request + +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/dataremote/model/request/RequestGetUserDto.kt b/app/src/main/java/org/sopt/and/data/dataremote/model/request/RequestGetUserDto.kt new file mode 100644 index 0000000..64e53b0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/request/RequestGetUserDto.kt @@ -0,0 +1,12 @@ +package org.sopt.and.data.dataremote.model.request + +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/dataremote/model/response/ResponseCreateUserDto.kt b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseCreateUserDto.kt new file mode 100644 index 0000000..ae376ee --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseCreateUserDto.kt @@ -0,0 +1,23 @@ +package org.sopt.and.data.dataremote.model.response + +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/dataremote/model/response/ResponseGetUserDto.kt b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseGetUserDto.kt new file mode 100644 index 0000000..15016f1 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseGetUserDto.kt @@ -0,0 +1,22 @@ +package org.sopt.and.data.dataremote.model.response + +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/dataremote/model/response/ResponseGetUserHobbySuccessDto.kt b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseGetUserHobbySuccessDto.kt new file mode 100644 index 0000000..7e201d1 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseGetUserHobbySuccessDto.kt @@ -0,0 +1,23 @@ +package org.sopt.and.data.dataremote.model.response + +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/dataremote/network/ApiFactory.kt b/app/src/main/java/org/sopt/and/data/dataremote/network/ApiFactory.kt new file mode 100644 index 0000000..8585fda --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/network/ApiFactory.kt @@ -0,0 +1,38 @@ +package org.sopt.and.data.dataremote.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 const val BASE_URL: String = "http://223.130.135.50:8085" + + 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/dataremote/network/UserService.kt b/app/src/main/java/org/sopt/and/data/dataremote/network/UserService.kt new file mode 100644 index 0000000..2c09cb4 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/network/UserService.kt @@ -0,0 +1,30 @@ +package org.sopt.and.data.dataremote.network + +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface UserService { + + @POST("/user") + suspend fun signUpUser( @Body requestCreateUserDto: RequestCreateUserDto ) + : Response + + @POST("/login") + suspend fun logInUser( @Body requestGetUserDto: 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/data/repositoryimpl/UserRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt new file mode 100644 index 0000000..6780411 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt @@ -0,0 +1,35 @@ +package org.sopt.and.data.repositoryimpl + +import jakarta.inject.Inject +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.data.dataremote.datasource.UserInfoRemoteDataSource +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.domain.repository.UserRepository +import retrofit2.Response + +class UserRepositoryImpl @Inject constructor( + private val userRemoteDataSource: UserInfoRemoteDataSource, + private val userLocalDataSource: UserInfoLocalDataSource +) : UserRepository { + override suspend fun postSignUp(requestCreateUserDto: RequestCreateUserDto): Response = + userRemoteDataSource.postSignup(requestCreateUserDto) + + override suspend fun postLogin(requestGetUserDto: RequestGetUserDto): Response = + userRemoteDataSource.postLogin(requestGetUserDto) + + override suspend fun getUserHobby(token: String): Response = + userRemoteDataSource.getUserHobby(token) + + override fun saveAccessToken(token: String) { + userLocalDataSource.accessToken = token + } + + override fun saveUserName(name: String) { + userLocalDataSource.userName = name + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/DataSourceModule.kt b/app/src/main/java/org/sopt/and/di/DataSourceModule.kt new file mode 100644 index 0000000..80b8910 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/DataSourceModule.kt @@ -0,0 +1,23 @@ +package org.sopt.and.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.data.datalocal.datasourceimpl.UserInfoLocalDataSourceImpl +import org.sopt.and.data.dataremote.datasource.UserInfoRemoteDataSource +import org.sopt.and.data.dataremote.datasourceimpl.UserInfoRemoteDataSourceImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataSourceModule { + @Binds + @Singleton + abstract fun bindsUserRemoteDataSource(userInfoRemoteDataSourceImpl: UserInfoRemoteDataSourceImpl): UserInfoRemoteDataSource + + @Binds + @Singleton + abstract fun bindsUserLocalDataSource(userInfoLocalDataSourceImpl: UserInfoLocalDataSourceImpl): UserInfoLocalDataSource +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt new file mode 100644 index 0000000..e82b8b4 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -0,0 +1,70 @@ +package org.sopt.and.di + +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.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.and.BuildConfig +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Provides + @Singleton + fun providesJson(): Json = + Json { + isLenient = true + prettyPrint = true + explicitNulls = false + ignoreUnknownKeys = true + } + + @Provides + @Singleton + fun providesOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient = + OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + writeTimeout(10, TimeUnit.SECONDS) + readTimeout(10, TimeUnit.SECONDS) + addInterceptor(loggingInterceptor) + addInterceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Accept", "*/*") + .build() + chain.proceed(request) + } + }.build() + + @Provides + @Singleton + fun providesLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + @ExperimentalSerializationApi + @Provides + @Singleton + fun providesRetrofit( + okHttpClient: OkHttpClient, + json: Json + ): Retrofit = + Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(okHttpClient) + .addConverterFactory( + json.asConverterFactory(requireNotNull("application/json".toMediaTypeOrNull())) + ) + .build() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/RepositoryModule.kt b/app/src/main/java/org/sopt/and/di/RepositoryModule.kt new file mode 100644 index 0000000..2a1d216 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/RepositoryModule.kt @@ -0,0 +1,17 @@ +package org.sopt.and.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.repositoryimpl.UserRepositoryImpl +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + @Singleton + abstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/ServiceModule.kt b/app/src/main/java/org/sopt/and/di/ServiceModule.kt new file mode 100644 index 0000000..44b11ac --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/ServiceModule.kt @@ -0,0 +1,18 @@ +package org.sopt.and.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.and.data.dataremote.network.UserService +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun providesService(retrofit: Retrofit): UserService = + retrofit.create(UserService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/model/User.kt b/app/src/main/java/org/sopt/and/domain/model/User.kt new file mode 100644 index 0000000..d02c199 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/User.kt @@ -0,0 +1,8 @@ +package org.sopt.and.domain.model + +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/domain/repository/UserRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt new file mode 100644 index 0000000..23f0b43 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt @@ -0,0 +1,17 @@ +package org.sopt.and.domain.repository + +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import retrofit2.Response + +interface UserRepository { + suspend fun postSignUp(requestCreateUserDto: RequestCreateUserDto): Response + suspend fun postLogin(requestGetUserDto: RequestGetUserDto): Response + suspend fun getUserHobby(token: String): Response + + fun saveAccessToken(token: String) + fun saveUserName(name: String) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/GetUserHobbyUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/GetUserHobbyUseCase.kt new file mode 100644 index 0000000..67d7d0f --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/GetUserHobbyUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.domain.repository.UserRepository +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GetUserHobbyUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(token: String): Response = + userRepository.getUserHobby(token = token) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/PostLoginUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/PostLoginUseCase.kt new file mode 100644 index 0000000..efba203 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/PostLoginUseCase.kt @@ -0,0 +1,16 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.data.dataremote.model.request.RequestGetUserDto +import org.sopt.and.data.dataremote.model.response.ResponseGetUserDto +import org.sopt.and.domain.repository.UserRepository +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PostLoginUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(requestGetUserDto: RequestGetUserDto): Response = + userRepository.postLogin(requestGetUserDto = requestGetUserDto) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/PostSignUpUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/PostSignUpUseCase.kt new file mode 100644 index 0000000..31851f6 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/PostSignUpUseCase.kt @@ -0,0 +1,16 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserSuccessDto +import org.sopt.and.domain.repository.UserRepository +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PostSignUpUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(requestCreateUserDto: RequestCreateUserDto): Response = + userRepository.postSignUp(requestCreateUserDto = requestCreateUserDto) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/SaveAccessTokenUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/SaveAccessTokenUseCase.kt new file mode 100644 index 0000000..2d09e80 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/SaveAccessTokenUseCase.kt @@ -0,0 +1,16 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.data.dataremote.model.response.ResponseGetUserHobbyDto +import org.sopt.and.domain.repository.UserRepository +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SaveAccessTokenUseCase @Inject constructor( + private val userRepository: UserRepository +) { + operator fun invoke(token: String) { + userRepository.saveAccessToken(token) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/SaveUserNameUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/SaveUserNameUseCase.kt new file mode 100644 index 0000000..b9cf56a --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/SaveUserNameUseCase.kt @@ -0,0 +1,14 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SaveUserNameUseCase @Inject constructor( + private val userRepository: UserRepository +) { + operator fun invoke(name: String) { + userRepository.saveUserName(name) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeContract.kt b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeContract.kt new file mode 100644 index 0000000..4a2c127 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeContract.kt @@ -0,0 +1,23 @@ +package org.sopt.and.presentation.homeScreen + +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class HomeContract { + + data class HomeUiState( + val pagerImages: List = listOf(), + val isLoading: Boolean = false + ) : UiState + + sealed interface HomeSideEffect : UiSideEffect { + data class NavigateToDetail(val imageIndex: Int) : HomeSideEffect + } + + sealed class HomeEvent : UiEvent { + data object OnScreenLoaded : HomeEvent() + data class OnImageClicked(val imageIndex: Int) : HomeEvent() + } +} + 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 51% 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..1ebd454 100644 --- a/app/src/main/java/org/sopt/and/HomeActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt @@ -1,190 +1,187 @@ -@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.Column -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.* import androidx.compose.foundation.pager.HorizontalPager 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.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle import androidx.navigation.NavController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable +import kotlinx.coroutines.flow.collectLatest +import org.sopt.and.R 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 = hiltViewModel() ) { - val context = LocalContext.current val scrollState = rememberScrollState() + val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + // SideEffect 처리 (네비게이션 및 단발성 이벤트) + LaunchedEffect(homeViewModel.sideEffect) { + homeViewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collectLatest { sideEffect -> + when (sideEffect) { + is HomeContract.HomeSideEffect.NavigateToDetail -> { + navController.navigate("detailScreen/${sideEffect.imageIndex}") + } + } + } + } + + // 화면이 로드될 때 이벤트 전송 + LaunchedEffect(Unit) { + homeViewModel.setEvent(HomeContract.HomeEvent.OnScreenLoaded) + } Scaffold( topBar = { - Column { + Column( + modifier = modifier.fillMaxWidth() + ) { CustomTopAppBar(navController = navController) CustomTopAppBarSecond(navController = navController) } }, - bottomBar = { - CustomBottomAppBar(navController = navController) - } - ) { innerPadding -> + bottomBar = { CustomBottomAppBar(navController = navController) } + ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) .background(Color(0xFF1B1B1B)) - .padding(innerPadding) // 패딩 적용 + .padding(paddingValues) .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 { uiState.pagerImages.size } HorizontalPager( state = pagerState, modifier = Modifier .fillMaxWidth() .height(400.dp) - ) { idx -> + ) { index -> Image( modifier = Modifier .fillMaxSize() .padding(16.dp) - .clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = images[idx]), + .clip(androidx.compose.foundation.shape.RoundedCornerShape(16.dp)), + painter = painterResource(id = uiState.pagerImages[index]), contentDescription = "imagePager", contentScale = ContentScale.Crop ) } + Spacer(modifier = Modifier.height(10.dp)) + HomeLazyRow( title = "믿고 보는 웨이브 에디터 추천작", - images = images, + images = uiState.pagerImages, height = 230, width = 140, + onItemClick = { index -> + homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index)) + } ) + Spacer(modifier = Modifier.height(10.dp)) HomeLazyRow( title = "실시간 인기 콘텐츠", - images = images, + images = uiState.pagerImages, height = 230, width = 140, + onItemClick = { index -> + homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index)) + } ) + Spacer(modifier = Modifier.height(10.dp)) HomeLazyRow( title = "오직 웨이브에서", - images = images, + images = uiState.pagerImages, height = 230, width = 140, + onItemClick = { index -> + homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index)) + } ) + Spacer(modifier = Modifier.height(10.dp)) HomeLazyRow( title = "오늘의 TOP 20", - images = images, + images = uiState.pagerImages, height = 260, width = 180, + onItemClick = { index -> + homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index)) + } ) + Spacer(modifier = Modifier.height(10.dp)) HomeLazyRow( title = "당한 대로 갚아줄게", - images = images, + images = uiState.pagerImages, height = 230, width = 140, + onItemClick = { index -> + homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index)) + } ) - Spacer(modifier = Modifier.height(10.dp)) } } } + @Preview(showBackground = true) @Composable fun HomeScreenPreview() { val navController = rememberNavController() - ANDANDROIDTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Column{ - CustomTopAppBar(navController = navController) - CustomTopAppBarSecond(navController = navController) - - } - - }, - 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, - )} - } - } - + val uiState = HomeContract.HomeUiState( + pagerImages = listOf( + R.drawable.food_pic1, + R.drawable.food_pic2, + R.drawable.food_pic3, + R.drawable.food_pic4, + R.drawable.food_pic5 + ) + ) + + val mockViewModel = object : HomeViewModel() { + init { + setState { uiState } } } + + HomeScreen( + navController = navController, + homeViewModel = mockViewModel + ) } \ 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..aba4308 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeViewModel.kt @@ -0,0 +1,34 @@ +package org.sopt.and.presentation.homeScreen + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.R +import org.sopt.and.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +open class HomeViewModel @Inject constructor() : + BaseViewModel() { + + override fun createInitialState(): HomeContract.HomeUiState { + return HomeContract.HomeUiState( + pagerImages = listOf( + R.drawable.food_pic1, + R.drawable.food_pic2, + R.drawable.food_pic3, + R.drawable.food_pic4, + R.drawable.food_pic5 + ) + ) + } + + override suspend fun handleEvent(event: HomeContract.HomeEvent) { + when (event) { + is HomeContract.HomeEvent.OnImageClicked -> { + sendSideEffect(HomeContract.HomeSideEffect.NavigateToDetail(event.imageIndex)) + } + is HomeContract.HomeEvent.OnScreenLoaded -> { + setState { copy(isLoading = true) } + } + } + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginContract.kt b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginContract.kt new file mode 100644 index 0000000..d86ec6b --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginContract.kt @@ -0,0 +1,30 @@ +package org.sopt.and.presentation.loginScreen + +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class LoginContract { + + data class LoginUiState( + val userName: String = "", + val password: String = "", + val isUserNameValid: Boolean = false, + val isPasswordValid: Boolean = false, + val isLoading: Boolean = false, + val loginResult: Boolean? = null, + val shouldShowPassword: Boolean = false + ) : UiState + + sealed class LoginEvent : UiEvent { + data class OnUserNameChanged(val userName: String) : LoginEvent() + data class OnPasswordChanged(val password: String) : LoginEvent() + data object OnLoginButtonClicked : LoginEvent() + data object OnTogglePasswordVisibility : LoginEvent() + } + + sealed interface LoginSideEffect : UiSideEffect { + data object NavigateToHome : LoginSideEffect + data class ShowSnackbar(val message: String) : LoginSideEffect + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt new file mode 100644 index 0000000..cefa898 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt @@ -0,0 +1,126 @@ +package org.sopt.and.presentation.loginScreen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +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.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +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.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import org.sopt.and.R +import org.sopt.and.ui.components.SignUpandLogIn.SignUpTextField + +@Composable +fun LoginScreen( + navigateToHomeScreen: () -> Unit, + loginViewModel: LoginViewModel = hiltViewModel() +) { + val uiState = loginViewModel.uiState.collectAsState().value + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + // 사이드이펙트 처리 + LaunchedEffect(Unit) { + loginViewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + LoginContract.LoginSideEffect.NavigateToHome -> navigateToHomeScreen() + is LoginContract.LoginSideEffect.ShowSnackbar -> { + snackbarHostState.showSnackbar(sideEffect.message) + } + } + } + } + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.wavve_logo), + contentDescription = "Logo", + modifier = Modifier + .size(100.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 유저네임 입력 + SignUpTextField( + text = uiState.userName, + onValueChange = { loginViewModel.setEvent(LoginContract.LoginEvent.OnUserNameChanged(it)) }, + fieldType = "UserName", + conditionCheck = uiState.isUserNameValid, + placeholder = "유저 이름 (7자 이하)", + errMessage = "유저 이름은 7자 이하여야 합니다." + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 비밀번호 입력 + SignUpTextField( + text = uiState.password, + onValueChange = { loginViewModel.setEvent(LoginContract.LoginEvent.OnPasswordChanged(it)) }, + fieldType = "Password", + conditionCheck = uiState.isPasswordValid, + placeholder = "비밀번호 입력", + errMessage = "비밀번호는 8자 이상이어야 합니다.", + shouldShowPassword = uiState.shouldShowPassword, + onPasswordVisibilityChange = { + loginViewModel.setEvent(LoginContract.LoginEvent.OnTogglePasswordVisibility) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { loginViewModel.setEvent(LoginContract.LoginEvent.OnLoginButtonClicked) }, + colors = ButtonDefaults.buttonColors(containerColor = Color.Blue), + modifier = Modifier.fillMaxWidth() + ) { + Text("로그인", color = Color.White) + } + + if (uiState.isLoading) { + Spacer(modifier = Modifier.height(16.dp)) + Text("로딩 중...", color = Color.Gray, fontSize = 14.sp) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun LoginScreenPreview() { + LoginScreen(navigateToHomeScreen = {}) +} 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..68d486f --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginViewModel.kt @@ -0,0 +1,60 @@ +package org.sopt.and.presentation.loginScreen + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.domain.usecase.PostLoginUseCase +import org.sopt.and.domain.usecase.SaveAccessTokenUseCase +import org.sopt.and.domain.usecase.SaveUserNameUseCase +import org.sopt.and.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val postLoginUseCase: PostLoginUseCase, + private val saveUserNameUseCase: SaveUserNameUseCase, + private val saveAccessTokenUseCase: SaveAccessTokenUseCase +) : BaseViewModel() { + + override fun createInitialState() = LoginContract.LoginUiState() + + override suspend fun handleEvent(event: LoginContract.LoginEvent) { + when (event) { + is LoginContract.LoginEvent.OnUserNameChanged -> { + setState { copy(userName = event.userName, isUserNameValid = event.userName.length <= 7) } + } + is LoginContract.LoginEvent.OnPasswordChanged -> { + setState { copy(password = event.password, isPasswordValid = event.password.length >= 8) } + } + LoginContract.LoginEvent.OnTogglePasswordVisibility -> { + setState { copy(shouldShowPassword = !shouldShowPassword) } + } + LoginContract.LoginEvent.OnLoginButtonClicked -> { + attemptLogin() + } + } + } + + private suspend fun attemptLogin() { + setState { copy(isLoading = true) } + + val currentState = currentState + val requestDto = org.sopt.and.data.dataremote.model.request.RequestGetUserDto( + userName = currentState.userName, + password = currentState.password + ) + + try { + val response = postLoginUseCase(requestDto) + if (response.isSuccessful && response.body()?.result?.token != null) { + saveUserNameUseCase(currentState.userName) + saveAccessTokenUseCase(response.body()!!.result.token) + sendSideEffect(LoginContract.LoginSideEffect.NavigateToHome) + } else { + sendSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("유저 이름 혹은 비밀번호를 확인하세요.")) + } + } catch (e: Exception) { + sendSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("로그인 요청 중 오류가 발생했습니다.")) + } finally { + setState { copy(isLoading = false) } + } + } +} 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 59% 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..70a887a 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,36 @@ -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.hilt.navigation.compose.hiltViewModel 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 - @Composable fun MypageScreen( navController: NavController, - userViewModel: UserViewModel = viewModel() + mypageViewModel: MypageViewModel = hiltViewModel() ) { - - val context = LocalContext.current - val emailText = userViewModel.email.observeAsState("").value + val user by mypageViewModel.user.collectAsState() Scaffold( bottomBar = { @@ -90,7 +44,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..d866134 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageViewModel.kt @@ -0,0 +1,58 @@ +package org.sopt.and.presentation.mypageScreen + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +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.dataremote.network.ServicePool +import org.sopt.and.domain.model.User +import org.sopt.and.domain.usecase.GetUserHobbyUseCase +import javax.inject.Inject + +@HiltViewModel +class MypageViewModel @Inject constructor( + private val userInfoLocalDataSource: UserInfoLocalDataSource, + private val getUserHobbyUseCase: GetUserHobbyUseCase +) : 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 = getUserHobbyUseCase(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..47db4b8 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,52 @@ 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.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import org.sopt.and.domain.model.User import org.sopt.and.ui.components.SignUpandLogIn.SignUpTextField import org.sopt.and.ui.components.SignUpandLogIn.SocialLoginSection import org.sopt.and.ui.theme.ANDANDROIDTheme -@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 = hiltViewModel() ) { 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 +87,7 @@ fun SignUpScreen( Spacer(modifier = Modifier.weight(0.35f)) Text( - "이메일과 비밀번호만으로\nWavve를 즐길 수 있어요!", + "유저 이름, 비밀번호, 취미 입력만으로\nWavve를 즐길 수 있어요!", color = Color.White, fontSize = 21.sp ) @@ -100,37 +95,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 +154,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 +194,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..12945d6 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpViewModel.kt @@ -0,0 +1,100 @@ +package org.sopt.and.presentation.signupScreen + +import android.util.Log +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.json.Json +import org.sopt.and.domain.model.User +import org.sopt.and.data.dataremote.model.request.RequestCreateUserDto +import org.sopt.and.data.dataremote.model.response.ResponseCreateUserFailedDto +import org.sopt.and.data.dataremote.network.ServicePool +import org.sopt.and.domain.usecase.PostSignUpUseCase +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val postSignUpUseCase: PostSignUpUseCase +) : ViewModel() { + + 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 = postSignUpUseCase(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/HomeScreen/HomeLazyRow.kt b/app/src/main/java/org/sopt/and/ui/components/HomeScreen/HomeLazyRow.kt index a511ed2..f0a3ec7 100644 --- a/app/src/main/java/org/sopt/and/ui/components/HomeScreen/HomeLazyRow.kt +++ b/app/src/main/java/org/sopt/and/ui/components/HomeScreen/HomeLazyRow.kt @@ -29,7 +29,8 @@ fun HomeLazyRow( images: List, height: Int, width: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onItemClick: (Int) -> Unit = {} ) { Column( modifier = modifier 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/app/src/main/java/org/sopt/and/util/base/BaseViewModel.kt b/app/src/main/java/org/sopt/and/util/base/BaseViewModel.kt new file mode 100644 index 0000000..52d1dfb --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/base/BaseViewModel.kt @@ -0,0 +1,33 @@ +package org.sopt.and.util.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +open abstract class BaseViewModel : ViewModel() { + + abstract fun createInitialState(): State + + private val _uiState = MutableStateFlow(createInitialState()) + val uiState: StateFlow get() = _uiState.asStateFlow() + val currentState: State get() = _uiState.value + + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow get() = _sideEffect.asSharedFlow() + + protected abstract suspend fun handleEvent(event: Event) + + protected fun setState(reduce: State.() -> State) { + _uiState.value = _uiState.value.reduce() + } + + fun setEvent(event: Event) { + viewModelScope.launch { handleEvent(event) } + } + + protected fun sendSideEffect(effect: SideEffect) { + viewModelScope.launch { _sideEffect.emit(effect) } + } +} + diff --git a/app/src/main/java/org/sopt/and/util/base/UiEvent.kt b/app/src/main/java/org/sopt/and/util/base/UiEvent.kt new file mode 100644 index 0000000..3b79a87 --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/base/UiEvent.kt @@ -0,0 +1,4 @@ +package org.sopt.and.util.base + +interface UiEvent { +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/util/base/UiSideEffect.kt b/app/src/main/java/org/sopt/and/util/base/UiSideEffect.kt new file mode 100644 index 0000000..8b64cfd --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/base/UiSideEffect.kt @@ -0,0 +1,4 @@ +package org.sopt.and.util.base + +interface UiSideEffect { +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/util/base/UiState.kt b/app/src/main/java/org/sopt/and/util/base/UiState.kt new file mode 100644 index 0000000..0c77cc9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/base/UiState.kt @@ -0,0 +1,3 @@ +package org.sopt.and.util.base + +interface UiState 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..7480b87 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,11 @@ 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" +daggerHilt = "2.52" +hiltNavigationCompose = "1.2.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -27,11 +32,26 @@ 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" } +hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "daggerHilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "daggerHilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } + [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" } +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHilt" } +[bundles] +hilt = [ + "hilt", + "hilt-navigation-compose" +] \ No newline at end of file