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 @@ + + - - - - - - - - - - - - - + - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .background(Color(0xFF1B1B1B)) - .padding(innerPadding) // 패딩 적용 - .padding(all = 10.dp) - ) { - - val images = listOf( - R.drawable.food_pic1, - R.drawable.food_pic2, - R.drawable.food_pic3, - R.drawable.food_pic4, - R.drawable.food_pic5 - ) - - val pagerState = rememberPagerState { images.size } - - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() - .height(400.dp) - ) { idx -> - Image( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .clip(RoundedCornerShape(16.dp)), - painter = painterResource(id = images[idx]), - contentDescription = "imagePager", - contentScale = ContentScale.Crop - ) - } - - HomeLazyRow( - title = "믿고 보는 웨이브 에디터 추천작", - images = images, - height = 230, - width = 140, - ) - Spacer(modifier = Modifier.height(10.dp)) - - HomeLazyRow( - title = "실시간 인기 콘텐츠", - images = images, - height = 230, - width = 140, - ) - Spacer(modifier = Modifier.height(10.dp)) - - HomeLazyRow( - title = "오직 웨이브에서", - images = images, - height = 230, - width = 140, - ) - Spacer(modifier = Modifier.height(10.dp)) - - HomeLazyRow( - title = "오늘의 TOP 20", - images = images, - height = 260, - width = 180, - ) - Spacer(modifier = Modifier.height(10.dp)) - - HomeLazyRow( - title = "당한 대로 갚아줄게", - images = images, - height = 230, - width = 140, - ) - 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, - )} - } - } - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/LoginActivity.kt b/app/src/main/java/org/sopt/and/LoginActivity.kt deleted file mode 100644 index ca074c4..0000000 --- a/app/src/main/java/org/sopt/and/LoginActivity.kt +++ /dev/null @@ -1,195 +0,0 @@ -package org.sopt.and - -import androidx.activity.ComponentActivity -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.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.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.SnackbarResult -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -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 class LoginScreen( - val emailText: String, - val passwordText: String -) - -@Composable -fun LoginScreen( - modifier: Modifier = Modifier.fillMaxSize(), - scope: CoroutineScope, - snackbarHostState: SnackbarHostState, - emailText: String, - passwordText: String, - navigateToHomeScreen: () -> 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..274225a 100644 --- a/app/src/main/java/org/sopt/and/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/MainActivity.kt @@ -5,94 +5,87 @@ 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.runtime.Composable 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 org.sopt.and.presentation.homeScreen.HomeRoute +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.LoginRoute +import org.sopt.and.presentation.mypageScreen.MypageRoute +import org.sopt.and.presentation.searchScreen.SearchRoute +import org.sopt.and.presentation.signupScreen.SignUpRoute +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() - - NavHost( - navController = navController, - startDestination = SignUpScreen, - modifier = Modifier.padding(innerPadding) - ){ - composable { - SignUpScreen( - navigateToLoginScreen = { - emailText, passwordText -> navController.navigate(LoginScreen(emailText, passwordText)) - } - ) - } + MainScreen() + } + } + } +} - 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") - } - ) - } +@Composable +fun MainScreen(){ + val navController = rememberNavController() - composable("home") { - HomeScreen( - navController = navController, - ) - } + NavHost( + navController = navController, + startDestination = "home", + modifier = Modifier + ){ + composable("signup") { + SignUpRoute( + navigateToLoginScreen = { + navController.navigate("login") { + popUpTo("signup") { inclusive = true } + } + } + ) + } - composable("search") { - SearchScreen( - navController = navController - ) - } + composable("login") { + LoginRoute( + navigateToHomeScreen = { + navController.navigate("home") { + popUpTo("home") { inclusive = true } + } + } + ) + } - composable("profile") { + composable("home") { + HomeRoute( + navController = navController, + ) + } - MypageScreen( - navController = navController, - userViewModel = userViewModel - ) - } + composable("search") { + SearchRoute( + navController = navController, + ) + } - } - } - } + composable("mypage") { + MypageRoute( + navController = navController, + ) } } } + diff --git a/app/src/main/java/org/sopt/and/MyActivity.kt b/app/src/main/java/org/sopt/and/MyActivity.kt deleted file mode 100644 index 3790d39..0000000 --- a/app/src/main/java/org/sopt/and/MyActivity.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.sopt.and - -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.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable -import org.sopt.and.ui.components.BottomBar.CustomBottomAppBar -import org.sopt.and.ui.components.BottomBar.NavIcon -import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection -import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection2 -import org.sopt.and.ui.components.MypageScreen.MyPageSubSection -import org.sopt.and.ui.components.TopBar.CustomTopAppBar -import org.sopt.and.ui.theme.ANDANDROIDTheme -import androidx.compose.runtime.livedata.observeAsState - - -@Composable -fun MypageScreen( - navController: NavController, - userViewModel: UserViewModel = viewModel() -) { - - val context = LocalContext.current - val emailText = userViewModel.email.observeAsState("").value - - Scaffold( - bottomBar = { - CustomBottomAppBar(navController = navController) - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFF1B1B1B)) - .padding(innerPadding) - ) { - MyPageProfileSection( - deliveredEmail = emailText - ) - Spacer(modifier = Modifier.height(0.5.dp)) - MyPageProfileSection2( - sectionDescription = "첫 결제 시 첫 달 100원!", - connectedUrl = "" - ) - MyPageProfileSection2( - sectionDescription = "현재 보유하신 이용권이 없습니다.", - connectedUrl = "" - ) - Spacer(modifier = Modifier.height(0.5.dp)) - - MyPageSubSection( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - title = "전체 시청내역", - topic = "시청내역", - contentNumber = 0, /* 임시로 0개 고정함 */ - ) - - Spacer( - modifier = Modifier.weight(0.2f) - ) - - MyPageSubSection( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - title = "관심 프로그램", - topic = "관심 프로그램", - contentNumber = 0, /* 임시로 0개 고정함 */ - ) - - Spacer(modifier = Modifier.weight(1f)) - } - } -} - -@Preview(showBackground = true) -@Composable -fun MyPagePreview() { - val navController = rememberNavController() - - ANDANDROIDTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - CustomBottomAppBar(navController = navController) - } - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFF1B1B1B)) - .padding(innerPadding) - ) { - 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/SignUpActivity.kt b/app/src/main/java/org/sopt/and/SignUpActivity.kt deleted file mode 100644 index 166e5f1..0000000 --- a/app/src/main/java/org/sopt/and/SignUpActivity.kt +++ /dev/null @@ -1,192 +0,0 @@ -package org.sopt.and - -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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -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 kotlinx.serialization.Serializable -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 { - var isValid = false - val inputStr : CharSequence = email - val pattern = Patterns.EMAIL_ADDRESS - val matcher = pattern.matcher(inputStr) - if(matcher.matches()){ - isValid = true - } - 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() - return password.matches(pattern) - -} - -@Composable -fun SignUpScreen( - modifier: Modifier = Modifier, - navigateToLoginScreen: (emailText: String, passwordText: String) -> Unit, -) { - - val context = LocalContext.current - - var emailFlag = 0 - var passwordFlag = 0 //8~20자 이내 조건 확인 - var toastMessage = "" - - var emailText = remember { mutableStateOf("") } - var passwordText = remember { mutableStateOf("") } - - var shouldShowPassword = remember {mutableStateOf(false)} - var isEmailValid = remember { mutableStateOf(true) } - var isPasswordValid = remember { mutableStateOf(true) } - - Column( - modifier = Modifier - .fillMaxSize() - .background(Color(0xFF1B1B1B)) - .padding(25.dp), - ){ - Spacer(modifier = Modifier.height(20.dp)) - Text( - "회원가입", - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - fontSize = 18.sp, - color = Color.White - - ) - Spacer(modifier = Modifier.weight(0.35f)) - - Text( - "이메일과 비밀번호만으로\nWavve를 즐길 수 있어요!", - color = Color.White, - fontSize = 21.sp - ) - - Spacer(modifier = Modifier.weight(0.25f)) - - SignUpTextField( - text = emailText.value, - onValueChange = { newValue -> - emailText.value = newValue - isEmailValid.value = EmailValidCheck(emailText.value) - }, - fieldType = "Email", - conditionCheck = isEmailValid.value, - errMessage = "올바른 이메일 형식이 아닙니다.", - placeholder = "wavve@example.com", - descriptionText = "로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요.", - ) - - Spacer(modifier = Modifier.weight(0.15f)) - - SignUpTextField( - text = passwordText.value, - onValueChange = { newValue -> - passwordText.value = newValue - isPasswordValid.value = PasswordValidCheck(passwordText.value) - }, - fieldType = "Password", - conditionCheck = isPasswordValid.value, - errMessage = "올바른 비밀번호 형식이 아닙니다.", - placeholder = "Wavve 비밀번호 설정", - shouldShowPassword = shouldShowPassword.value, - onPasswordVisibilityChange = { - shouldShowPassword.value = !shouldShowPassword.value - }, - descriptionText = "비밀번호는 8~20자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해 주세요.", - ) - - Spacer(modifier = Modifier.weight(0.5f)) - SocialLoginSection(modifier = modifier) - Spacer(modifier = Modifier.weight(1f)) - - Text( - "Wavve 회원가입", - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .background(Color.DarkGray) - .padding(vertical = 13.dp) - .clickable { - - //이메일 형식 조건 검사 - if (!EmailValidCheck(emailText.value)) { - emailFlag = 1 - toastMessage = "형식에 맞는 이메일을 입력하세요" - - } - - //비밀번호 형식 조건 검사 - if (!PasswordValidCheck(passwordText.value)) { - passwordFlag = 1 - toastMessage = "조건에 맞는 비밀번호를 사용하세요" - } - - if (emailFlag == 0 && passwordFlag == 0) { - - toastMessage = "로그인 되었습니다" - - //전달해줄 인자를 이 안에 넣으면 되는 듯.. - navigateToLoginScreen(emailText.value, passwordText.value) - println("네비게이트는 지남...") - } - - Toast - .makeText(context, toastMessage, Toast.LENGTH_SHORT) - .show() - - }, - color = Color.White - ) - } -} - - - -@Preview(showBackground = true) -@Composable -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/data/datalocal/datasource/UserInfoLocalDataSource.kt b/app/src/main/java/org/sopt/and/data/datalocal/datasource/UserInfoLocalDataSource.kt new file mode 100644 index 0000000..a791b58 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/datalocal/datasource/UserInfoLocalDataSource.kt @@ -0,0 +1,8 @@ +package org.sopt.and.data.datalocal.datasource + +interface UserInfoLocalDataSource { + var accessToken: String + var userName: String + var hobby: 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..88985fe --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/datalocal/datasourceimpl/UserInfoLocalDataSourceImpl.kt @@ -0,0 +1,37 @@ +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 var hobby: String + get() = sharedPreferences.getString(HOBBY, INITIAL_VALUE).toString() + set(value) = sharedPreferences.edit { putString(HOBBY, 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 HOBBY = "hobby" + 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..cac716c --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/model/response/ResponseCreateUserDto.kt @@ -0,0 +1,17 @@ +package org.sopt.and.data.dataremote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@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..9ae62bb --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/dataremote/network/ApiFactory.kt @@ -0,0 +1,36 @@ +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 org.sopt.and.BuildConfig +import retrofit2.Retrofit + +object ApiFactory { + private const val BASE_URL: String = BuildConfig.BASE_URL + + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val client = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + inline fun create(): T = retrofit.create(T::class.java) +} + +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/mapper/UserMapper.kt b/app/src/main/java/org/sopt/and/data/mapper/UserMapper.kt new file mode 100644 index 0000000..428bc68 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/mapper/UserMapper.kt @@ -0,0 +1,41 @@ +package org.sopt.and.data.mapper + +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.domain.model.LoginResult +import org.sopt.and.domain.model.SignUpResult +import org.sopt.and.domain.model.User + +object UserMapper { + + fun toRequestCreateUserDto(user: User): RequestCreateUserDto { + return RequestCreateUserDto( + userName = user.name, + password = user.password, + hobby = user.hobby + ) + } + + fun toRequestGetUserDto(user: User): RequestGetUserDto { + return RequestGetUserDto( + userName = user.name, + password = user.password + ) + } + + fun toSignUpResult(response: ResponseCreateUserSuccessDto): SignUpResult { + return SignUpResult( + userId = response.result.no + ) + } + + fun toLoginResult(response: ResponseGetUserDto): LoginResult { + return LoginResult( + accessToken = response.result.token + ) + } + +} + 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..5b9ecaf --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repositoryimpl/UserRepositoryImpl.kt @@ -0,0 +1,54 @@ +package org.sopt.and.data.repositoryimpl + +import jakarta.inject.Inject +import kotlinx.serialization.json.Json +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.data.dataremote.datasource.UserInfoRemoteDataSource +import org.sopt.and.data.mapper.UserMapper +import org.sopt.and.domain.model.LoginResult +import org.sopt.and.domain.model.SignUpResult +import org.sopt.and.domain.model.User +import org.sopt.and.domain.repository.UserRepository + +class UserRepositoryImpl @Inject constructor( + private val userRemoteDataSource: UserInfoRemoteDataSource, + private val userLocalDataSource: UserInfoLocalDataSource +) : UserRepository { + override suspend fun postSignUp(user: User): SignUpResult { + val requestDto = UserMapper.toRequestCreateUserDto(user) + val response = userRemoteDataSource.postSignup(requestDto) + + return if (response.isSuccessful) { + response.body()?.let { UserMapper.toSignUpResult(it) } + ?: throw IllegalStateException("회원가입 실패") + } else { + val errorCode = response.errorBody()?.string()?.let { errorBody -> + Json.decodeFromString>(errorBody)["code"] + } + SignUpResult(userId = null, errorCode = errorCode ?: "UNKNOWN_ERROR") + } + } + + override suspend fun postLogin(user: User): LoginResult { + val requestDto = UserMapper.toRequestGetUserDto(user) + val response = userRemoteDataSource.postLogin(requestDto) + + return response.body()?.let { UserMapper.toLoginResult(it) } + ?: throw IllegalStateException("로그인 실패") + } + + override suspend fun getUserHobby(token: String): String? { + val response = userRemoteDataSource.getUserHobby(token) + return response.body()?.result?.userHobby + } + + + 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..929ccc0 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -0,0 +1,64 @@ +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) + }.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/LoginResult.kt b/app/src/main/java/org/sopt/and/domain/model/LoginResult.kt new file mode 100644 index 0000000..44d73e6 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/LoginResult.kt @@ -0,0 +1,5 @@ +package org.sopt.and.domain.model + +data class LoginResult ( + val accessToken: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/model/SignUpResult.kt b/app/src/main/java/org/sopt/and/domain/model/SignUpResult.kt new file mode 100644 index 0000000..ec45654 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/SignUpResult.kt @@ -0,0 +1,8 @@ +package org.sopt.and.domain.model + +data class SignUpResult ( + val userId: Int? = null, + val errorCode: String? = null +){ + val isSuccessful: Boolean get() = userId != null +} \ 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..11ffa7e --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/repository/UserRepository.kt @@ -0,0 +1,14 @@ +package org.sopt.and.domain.repository + +import org.sopt.and.domain.model.LoginResult +import org.sopt.and.domain.model.SignUpResult +import org.sopt.and.domain.model.User + +interface UserRepository { + suspend fun postSignUp(user: User): SignUpResult + suspend fun postLogin(user: User): LoginResult + suspend fun getUserHobby(token: String): String? + + 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..eda90a8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/GetUserHobbyUseCase.kt @@ -0,0 +1,13 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GetUserHobbyUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(token: String): String? = + userRepository.getUserHobby(token = token) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/usecase/GetUserInfoUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/GetUserInfoUseCase.kt new file mode 100644 index 0000000..0c40afa --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/GetUserInfoUseCase.kt @@ -0,0 +1,16 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.domain.model.User +import javax.inject.Inject + +class GetUserInfoUseCase @Inject constructor( + private val userInfoLocalDataSource: UserInfoLocalDataSource +){ + operator fun invoke(): User { + val userName = userInfoLocalDataSource.userName + val accessToken = userInfoLocalDataSource.accessToken + val hobby = userInfoLocalDataSource.hobby + return User(userName, accessToken, hobby) + } +} 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..dc7896c --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/PostLoginUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.domain.model.LoginResult +import org.sopt.and.domain.model.User +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PostLoginUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(user: User): LoginResult = + userRepository.postLogin(user) +} \ 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..f2d4842 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/PostSignUpUseCase.kt @@ -0,0 +1,15 @@ +package org.sopt.and.domain.usecase + +import org.sopt.and.domain.model.SignUpResult +import org.sopt.and.domain.model.User +import org.sopt.and.domain.repository.UserRepository +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PostSignUpUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(user: User): SignUpResult = + userRepository.postSignUp(user) +} \ 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..c564f3f --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/SaveAccessTokenUseCase.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 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/domain/usecase/ValidateUserInputUseCase.kt b/app/src/main/java/org/sopt/and/domain/usecase/ValidateUserInputUseCase.kt new file mode 100644 index 0000000..ab1f5d8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/usecase/ValidateUserInputUseCase.kt @@ -0,0 +1,20 @@ +package org.sopt.and.domain.usecase + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ValidateUserInputUseCase @Inject constructor() { + companion object { + const val MAX_USERNAME_LENGTH = 7 + } + + fun stringInputValidCheck(userName: String) : Boolean { + return userName.length <= MAX_USERNAME_LENGTH + } + + fun passwordValidCheck(password: String) : Boolean { + val pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#\$%^&*]).{8,20}$".toRegex() + return password.matches(pattern) + } +} \ 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..a7fc59a --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeContract.kt @@ -0,0 +1,25 @@ +package org.sopt.and.presentation.homeScreen + +import androidx.compose.runtime.Immutable +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class HomeContract { + + @Immutable + 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/presentation/homeScreen/HomeScreen.kt b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt new file mode 100644 index 0000000..1195b53 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/homeScreen/HomeScreen.kt @@ -0,0 +1,191 @@ +package org.sopt.and.presentation.homeScreen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +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.verticalScroll +import androidx.compose.material3.Scaffold +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.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.rememberNavController +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 + +@Composable +fun HomeRoute( + homeViewModel: HomeViewModel = hiltViewModel(), + navController: NavController, +){ + val scrollState = rememberScrollState() + val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + 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) + } + + HomeScreen( + scrollState = scrollState, + uiState = uiState, + navController = navController, + onItemClick = { index -> homeViewModel.setEvent(HomeContract.HomeEvent.OnImageClicked(index))} + ) + +} + +@Composable +fun HomeScreen( + scrollState: ScrollState, + uiState: HomeContract.HomeUiState, + navController: NavController, + modifier: Modifier = Modifier, + onItemClick: (Int) -> Unit +) { + + Scaffold( + topBar = { + Column( + modifier = modifier.fillMaxWidth() + ) { + CustomTopAppBar() + CustomTopAppBarSecond() + } + }, + bottomBar = { CustomBottomAppBar(navController = navController) } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .background(Color(0xFF1B1B1B)) + .padding(paddingValues) + .padding(all = 10.dp) + ) { + val pagerState = rememberPagerState { uiState.pagerImages.size } + + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + ) { index -> + Image( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .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 = uiState.pagerImages, + height = 230, + width = 140, + onItemClick = onItemClick + ) + + Spacer(modifier = Modifier.height(10.dp)) + + HomeLazyRow( + title = "실시간 인기 콘텐츠", + images = uiState.pagerImages, + height = 230, + width = 140, + onItemClick = onItemClick + ) + + Spacer(modifier = Modifier.height(10.dp)) + + HomeLazyRow( + title = "오직 웨이브에서", + images = uiState.pagerImages, + height = 230, + width = 140, + onItemClick = onItemClick + ) + + Spacer(modifier = Modifier.height(10.dp)) + + HomeLazyRow( + title = "오늘의 TOP 20", + images = uiState.pagerImages, + height = 260, + width = 180, + onItemClick = onItemClick + ) + + Spacer(modifier = Modifier.height(10.dp)) + + HomeLazyRow( + title = "당한 대로 갚아줄게", + images = uiState.pagerImages, + height = 230, + width = 140, + onItemClick = onItemClick + ) + } + } +} + + + + +//@Preview(showBackground = true) +//@Composable +//fun HomeScreenPreview() { +// val navController = rememberNavController() +// +// 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 scrollState = rememberScrollState() +// +// HomeScreen( +// scrollState = scrollState, +// uiState = uiState, +// modifier = Modifier, +// onItemClick = { } +// ) +//} \ 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..242a122 --- /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 -> { + setSideEffect(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..aabfe63 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginContract.kt @@ -0,0 +1,31 @@ +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..2beab79 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginScreen.kt @@ -0,0 +1,160 @@ +package org.sopt.and.presentation.loginScreen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.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.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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +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 +import org.sopt.and.ui.components.SignUpandLogIn.SocialLoginSection + +@Composable +fun LoginRoute( + navigateToHomeScreen: () -> Unit, + loginViewModel: LoginViewModel = hiltViewModel() +){ + val uiState = loginViewModel.uiState.collectAsState().value + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + loginViewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + LoginContract.LoginSideEffect.NavigateToHome -> navigateToHomeScreen() + is LoginContract.LoginSideEffect.ShowSnackbar -> { + snackbarHostState.showSnackbar(sideEffect.message) + } + } + } + } + + LoginScreen( + uiState = uiState, + snackbarHostState = snackbarHostState, + onUserNameChanged = { loginViewModel.setEvent(LoginContract.LoginEvent.OnUserNameChanged(it)) }, + onPasswordChanged = { loginViewModel.setEvent(LoginContract.LoginEvent.OnPasswordChanged(it)) }, + onTogglePasswordVisibility = {loginViewModel.setEvent(LoginContract.LoginEvent.OnTogglePasswordVisibility)}, + onLoginButtonClicked = {loginViewModel.setEvent(LoginContract.LoginEvent.OnLoginButtonClicked)}, + ) +} + + +@Composable +fun LoginScreen( + uiState: LoginContract.LoginUiState, + snackbarHostState: SnackbarHostState, + onUserNameChanged: (String) -> Unit, + onPasswordChanged: (String) -> Unit, + onTogglePasswordVisibility: () -> Unit, + onLoginButtonClicked: () -> Unit, +) { + + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF1B1B1B)) + .padding(innerPadding) + .padding(25.dp) + ) { + Spacer(modifier = Modifier.height(10.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.wavve_logo), + contentDescription = "Logo", + modifier = Modifier + .size(100.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + SignUpTextField( + text = uiState.userName, + onValueChange = onUserNameChanged, + fieldType = "UserName", + conditionCheck = uiState.isUserNameValid, + placeholder = "유저 이름 (7자 이하)", + errMessage = "유저 이름은 7자 이하여야 합니다." + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SignUpTextField( + text = uiState.password, + onValueChange = onPasswordChanged, + fieldType = "Password", + conditionCheck = uiState.isPasswordValid, + placeholder = "비밀번호 입력", + errMessage = "비밀번호는 8자 이내여야 합니다.", + shouldShowPassword = uiState.shouldShowPassword, + onPasswordVisibilityChange = onTogglePasswordVisibility + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onLoginButtonClicked, + colors = ButtonDefaults.buttonColors(containerColor = Color.Blue), + modifier = Modifier.fillMaxWidth() + ) { + Text("로그인", color = Color.White) + } + + 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 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..44f0b14 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/loginScreen/LoginViewModel.kt @@ -0,0 +1,74 @@ +package org.sopt.and.presentation.loginScreen + +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.domain.model.User +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.domain.usecase.ValidateUserInputUseCase +import org.sopt.and.util.base.BaseViewModel +import retrofit2.HttpException +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val postLoginUseCase: PostLoginUseCase, + private val validateUserInputUseCase: ValidateUserInputUseCase, + 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 -> { + val isValid = validateUserInputUseCase.stringInputValidCheck(event.userName) + setState { copy(userName = event.userName, isUserNameValid = isValid) } + } + is LoginContract.LoginEvent.OnPasswordChanged -> { + val isValid = validateUserInputUseCase.passwordValidCheck(event.password) + setState { copy(password = event.password, isPasswordValid = isValid) } + } + LoginContract.LoginEvent.OnTogglePasswordVisibility -> { + setState { copy(shouldShowPassword = !shouldShowPassword) } + } + LoginContract.LoginEvent.OnLoginButtonClicked -> { + setState { copy(isLoading = true) } + attemptLogin() + } + } + } + + private suspend fun attemptLogin() { + + val currentState = currentState + + try { + val user = User( + name = currentState.userName, + password = currentState.password, + hobby = "" + ) + + val loginResult = postLoginUseCase(user) + + saveUserNameUseCase(currentState.userName) + saveAccessTokenUseCase(loginResult.accessToken) + setSideEffect(LoginContract.LoginSideEffect.NavigateToHome) + setSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("로그인에 성공했습니다.")) + + } catch (e: HttpException) { + when (e.code()){ + 400 -> setSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("로그인 요청 정보가 올바르지 않습니다.")) + 403 -> setSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("유저 이름 혹은 비밀번호가 일치하지 않습니다.")) + 404 -> setSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("유효하지 않은 경로로 요청이 들어왔습니다.\n 처음부터 재시도하세요.")) + } + } catch (e: Exception) { + setSideEffect(LoginContract.LoginSideEffect.ShowSnackbar("로그인 요청 중 오류가 발생했습니다.")) + } finally { + setState { copy(isLoading = false) } + } + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageContract.kt b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageContract.kt new file mode 100644 index 0000000..8bab821 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageContract.kt @@ -0,0 +1,23 @@ +package org.sopt.and.presentation.mypageScreen + +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class MypageContract { + + data class MyPageUiState( + val userName: String = "", + val userHobby: String = "", + val accessToken: String = "", + val contentCount: Int = 0, + ) : UiState + + sealed class MyPageEvent : UiEvent { + data object OnLoadUserData : MyPageEvent() + } + + sealed interface MyPageSideEffect : UiSideEffect { + data object ShowErrorToast : MyPageSideEffect + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt new file mode 100644 index 0000000..491e104 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageScreen.kt @@ -0,0 +1,110 @@ +package org.sopt.and.presentation.mypageScreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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.navigation.NavController +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.launch +import org.sopt.and.ui.components.BottomBar.CustomBottomAppBar +import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection +import org.sopt.and.ui.components.MypageScreen.MyPageProfileSection2 +import org.sopt.and.ui.components.MypageScreen.MyPageSubSection + +@Composable +fun MypageRoute( + navController: NavController, + mypageViewModel: MypageViewModel = hiltViewModel() +){ + val uiState by mypageViewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + mypageViewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + MypageContract.MyPageSideEffect.ShowErrorToast -> { + coroutineScope.launch { + snackbarHostState.showSnackbar("사용자 데이터를 불러오는 중 오류가 발생했습니다.") + } + } + } + } + } + + MypageScreen( + navController = navController, + snackbarHostState = snackbarHostState, + myPageUiState = uiState, + ) +} + +@Composable +fun MypageScreen( + navController: NavController, + snackbarHostState: SnackbarHostState, + myPageUiState: MypageContract.MyPageUiState, +) { + + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { CustomBottomAppBar(navController = navController) } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF1B1B1B)) + .padding(innerPadding) + ) { + MyPageProfileSection( + deliveredUserName = myPageUiState.userName, + deliveredUserHobby = myPageUiState.userHobby + ) + Spacer(modifier = Modifier.height(0.5.dp)) + MyPageProfileSection2( + sectionDescription = "첫 결제 시 첫 달 100원!", + connectedUrl = "" + ) + MyPageProfileSection2( + sectionDescription = "현재 보유하신 이용권이 없습니다.", + connectedUrl = "" + ) + Spacer(modifier = Modifier.height(0.5.dp)) + MyPageSubSection( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + title = "전체 시청내역", + topic = "시청내역", + contentNumber = myPageUiState.contentCount + ) + Spacer(modifier = Modifier.height(16.dp)) + MyPageSubSection( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + title = "관심 프로그램", + topic = "관심 프로그램", + contentNumber = 0 // 임시값 + ) + } + } +} + +//@Preview(showBackground = true) +//@Composable +//fun MyPagePreview() { +// val navController = rememberNavController() +// +// MypageScreen(navController = navController) +//} + + 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..34ff033 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypageScreen/MypageViewModel.kt @@ -0,0 +1,49 @@ +package org.sopt.and.presentation.mypageScreen + +import android.util.Log +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.domain.usecase.GetUserHobbyUseCase +import org.sopt.and.domain.usecase.GetUserInfoUseCase +import org.sopt.and.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class MypageViewModel @Inject constructor( + private val getUserInfoUseCase: GetUserInfoUseCase, + private val getUserHobbyUseCase: GetUserHobbyUseCase +) : BaseViewModel() { + + override fun createInitialState(): MypageContract.MyPageUiState = MypageContract.MyPageUiState() + + init { + setEvent(MypageContract.MyPageEvent.OnLoadUserData) + } + + override suspend fun handleEvent(event: MypageContract.MyPageEvent) { + when (event) { + MypageContract.MyPageEvent.OnLoadUserData -> loadUserData() + } + } + + private fun loadUserData() { + viewModelScope.launch { + try { + val userInfo = getUserInfoUseCase() + val hobby = getUserHobbyUseCase(userInfo.accessToken) + + setState { + copy( + userName = userName, + accessToken = accessToken, + userHobby = hobby ?: "취미 없음" + ) + } + } catch (e: Exception) { + Log.e("MyPage", "Error loading user data: ${e.message}") + setSideEffect(MypageContract.MyPageSideEffect.ShowErrorToast) + } + } + } +} 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 85% 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..cf446fe 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 @@ -14,6 +14,15 @@ import androidx.navigation.compose.rememberNavController import org.sopt.and.ui.components.BottomBar.CustomBottomAppBar import org.sopt.and.ui.theme.ANDANDROIDTheme +@Composable +fun SearchRoute( + navController: NavController, +){ + SearchScreen( + navController = navController + ) +} + @Composable fun SearchScreen( modifier: Modifier = Modifier, @@ -26,7 +35,7 @@ fun SearchScreen( } ) { innerPadding -> Text( - text = "검색 페이지\n입니다........................\n..........", + text = "검색 페이지 입니다.", modifier = Modifier .padding(innerPadding) .background(Color(0xFF1B1B1B)) @@ -40,7 +49,6 @@ fun SearchScreen( @Composable fun SearchScreenPreview() { val navController = rememberNavController() - ANDANDROIDTheme { SearchScreen( navController = navController diff --git a/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpContract.kt b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpContract.kt new file mode 100644 index 0000000..2471048 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpContract.kt @@ -0,0 +1,33 @@ +package org.sopt.and.presentation.signupScreen + +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class SignUpContract { + + data class SignUpUiState( + val userName: String = "", + val password: String = "", + val hobby: String = "", + val isUserNameValid: Boolean = true, + val isPasswordValid: Boolean = true, + val isHobbyValid: Boolean = true, + val shouldShowPassword: Boolean = false, + val isLoading: Boolean? = false, + ) : UiState + + sealed class SignUpEvent : UiEvent { + data class OnUserNameChanged(val userName: String) : SignUpEvent() + data class OnPasswordChanged(val password: String) : SignUpEvent() + data class OnHobbyChanged(val hobby: String) : SignUpEvent() + data object OnTogglePasswordVisibility : SignUpEvent() + data object OnSignUpButtonClicked : SignUpEvent() + } + + sealed interface SignUpSideEffect : UiSideEffect { + data object ShowSuccessToast : SignUpSideEffect + data class ShowErrorToast(val message: String) : SignUpSideEffect + data object NavigateToLoginScreen : SignUpSideEffect + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt new file mode 100644 index 0000000..aeaa1c5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpScreen.kt @@ -0,0 +1,162 @@ +package org.sopt.and.presentation.signupScreen + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.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.compose.collectAsStateWithLifecycle +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 + +@Composable +fun SignUpRoute( + navigateToLoginScreen: () -> Unit, + signUpViewModel: SignUpViewModel = hiltViewModel() +){ + val uiState by signUpViewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + + // SideEffect 감지 + LaunchedEffect(Unit) { + signUpViewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is SignUpContract.SignUpSideEffect.ShowSuccessToast -> { + Toast.makeText(context, "회원가입에 성공했습니다.", Toast.LENGTH_SHORT).show() + } + is SignUpContract.SignUpSideEffect.ShowErrorToast -> { + Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() + } + is SignUpContract.SignUpSideEffect.NavigateToLoginScreen -> { + navigateToLoginScreen() + } + } + } + } + + SignUpScreen( + uiState = uiState, + onUserNameChanged = { signUpViewModel.setEvent(SignUpContract.SignUpEvent.OnUserNameChanged(it)) }, + onPasswordChanged = { signUpViewModel.setEvent(SignUpContract.SignUpEvent.OnPasswordChanged(it)) }, + onTogglePasswordVisibility = { signUpViewModel.setEvent(SignUpContract.SignUpEvent.OnTogglePasswordVisibility) }, + onHobbyChanged = { signUpViewModel.setEvent(SignUpContract.SignUpEvent.OnHobbyChanged(it)) }, + onSignUpButtonClicked = { signUpViewModel.setEvent(SignUpContract.SignUpEvent.OnSignUpButtonClicked) } + ) + +} + +@Composable +fun SignUpScreen( + uiState: SignUpContract.SignUpUiState, + onUserNameChanged: (String) -> Unit, + onPasswordChanged: (String) -> Unit, + onTogglePasswordVisibility: () -> Unit, + onHobbyChanged: (String) -> Unit, + onSignUpButtonClicked: () -> Unit, +) { + + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF1B1B1B)) + .padding(25.dp) + ) { + Spacer(modifier = Modifier.height(20.dp)) + Text( + "회원가입", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontSize = 18.sp, + color = Color.White + + ) + Spacer(modifier = Modifier.weight(0.35f)) + Text( + "유저 이름, 비밀번호, 취미 입력만으로\nWavve를 즐길 수 있어요!", + color = Color.White, + fontSize = 21.sp + ) + Spacer(modifier = Modifier.weight(0.25f)) + + SignUpTextField( + text = uiState.userName, + onValueChange = onUserNameChanged, + fieldType = "Username", + conditionCheck = uiState.isUserNameValid, + errMessage = "유저 이름은 7자 이하여야 합니다.", + placeholder = "유저 이름 (7자 이하)", + descriptionText = "로그인, 비밀번호 찾기, 알림에 사용되니 정확하게 입력해주세요." + ) + Spacer(modifier = Modifier.weight(0.15f)) + + SignUpTextField( + text = uiState.password, + onValueChange = onPasswordChanged, + fieldType = "Password", + conditionCheck = uiState.isPasswordValid, + errMessage = "비밀번호는 8~20자 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.", + placeholder = "비밀번호 입력", + shouldShowPassword = uiState.shouldShowPassword, + onPasswordVisibilityChange = onTogglePasswordVisibility, + descriptionText = "비밀번호는 8~20자 이내로 영문, 숫자, 특수문자 중 3가지 이상 혼용해주세요." + ) + SignUpTextField( + text = uiState.hobby, + onValueChange = onHobbyChanged, + fieldType = "Hobby", + conditionCheck = uiState.isHobbyValid, + errMessage = "취미는 7자 이하여야 합니다.", + placeholder = "취미 입력" + ) + + Spacer(modifier = Modifier.weight(0.5f)) + SocialLoginSection(modifier = Modifier) + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "Wavve 회원가입", + modifier = Modifier + .fillMaxWidth() + .background(Color.DarkGray) + .padding(13.dp) + .clickable { onSignUpButtonClicked() }, + color = Color.White, + textAlign = TextAlign.Center + ) + } +} + +//@Preview(showBackground = true) +//@Composable +//fun SignUpScreenPreview() { +// SignUpScreen( +// navigateToLoginScreen = { } +// ) +//} 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..2a1d911 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/signupScreen/SignUpViewModel.kt @@ -0,0 +1,73 @@ +package org.sopt.and.presentation.signupScreen + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.domain.model.User +import org.sopt.and.domain.usecase.PostSignUpUseCase +import org.sopt.and.domain.usecase.ValidateUserInputUseCase +import org.sopt.and.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val postSignUpUseCase: PostSignUpUseCase, + private val validateUserInputUseCase: ValidateUserInputUseCase +) : BaseViewModel() { + + override fun createInitialState(): SignUpContract.SignUpUiState = SignUpContract.SignUpUiState() + + override suspend fun handleEvent(event: SignUpContract.SignUpEvent) { + when (event) { + is SignUpContract.SignUpEvent.OnUserNameChanged -> { + val isValid = validateUserInputUseCase.stringInputValidCheck(event.userName) + setState { copy(userName = event.userName, isUserNameValid = isValid) } + } + is SignUpContract.SignUpEvent.OnPasswordChanged -> { + val isValid = validateUserInputUseCase.passwordValidCheck(event.password) + setState { copy(password = event.password, isPasswordValid = isValid) } + } + is SignUpContract.SignUpEvent.OnHobbyChanged -> { + val isValid = validateUserInputUseCase.stringInputValidCheck(event.hobby) + setState { copy(hobby = event.hobby, isHobbyValid = isValid) } + } + is SignUpContract.SignUpEvent.OnTogglePasswordVisibility -> { + setState { copy(shouldShowPassword = !shouldShowPassword) } + } + is SignUpContract.SignUpEvent.OnSignUpButtonClicked -> { + viewModelScope.launch { + setState { copy(isLoading = true) } + attemptSignUp() + } + } + } + } + + private fun attemptSignUp() { + viewModelScope.launch { + + try { + val currentState = currentState + val user = User( + name = currentState.userName, + password = currentState.password, + hobby = currentState.hobby + ) + + val signUpResult = postSignUpUseCase(user) + + if (signUpResult.isSuccessful) { + setSideEffect(SignUpContract.SignUpSideEffect.ShowSuccessToast) + setSideEffect(SignUpContract.SignUpSideEffect.NavigateToLoginScreen) + } else { + SignUpContract.SignUpSideEffect.ShowErrorToast("회원가입 실패") + } + + } catch (e: Exception) { + setSideEffect(SignUpContract.SignUpSideEffect.ShowErrorToast("오류: ${e.message}")) + } finally { + setState { copy(isLoading = false) } + } + } + } +} 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..9172564 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(), @@ -28,19 +27,22 @@ fun CustomBottomAppBar(navController: NavController) { navController = navController, route = "home", icon = Icons.Filled.Home, - text = "홈" + text = "홈", + pageIndex = 0 ) NavIcon( navController = navController, route = "search", icon = Icons.Filled.Search, - text = "검색" + text = "검색", + pageIndex = 1 ) NavIcon( navController = navController, route = "profile", icon = Icons.Filled.Person, - text = "MY" + text = "MY", + pageIndex = 2 ) } } diff --git a/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarContract.kt b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarContract.kt new file mode 100644 index 0000000..fadc3d5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarContract.kt @@ -0,0 +1,23 @@ +package org.sopt.and.ui.components.BottomBar + +import org.sopt.and.util.base.UiEvent +import org.sopt.and.util.base.UiSideEffect +import org.sopt.and.util.base.UiState + +class NavBarContract { + + data class NavBarUiState( + val userName: String = "", + val accessToken: String = "", + val page: Int = 0, + ) : UiState + + sealed class NavBarEvent : UiEvent { + data object OnLoadUserData : NavBarEvent() + data class OnPageSelected(val pageIndex: Int) : NavBarEvent() + } + + sealed interface NavBarSideEffect : UiSideEffect { + data object ShowErrorToast : NavBarSideEffect + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarViewModel.kt b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarViewModel.kt new file mode 100644 index 0000000..4394875 --- /dev/null +++ b/app/src/main/java/org/sopt/and/ui/components/BottomBar/NavBarViewModel.kt @@ -0,0 +1,55 @@ +package org.sopt.and.ui.components.BottomBar + +import android.util.Log +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.data.datalocal.datasource.UserInfoLocalDataSource +import org.sopt.and.presentation.mypageScreen.MypageContract +import org.sopt.and.util.base.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class NavBarViewModel @Inject constructor( + private val userInfoLocalDataSource: UserInfoLocalDataSource +) : BaseViewModel() { + + override fun createInitialState(): NavBarContract.NavBarUiState = NavBarContract.NavBarUiState() + + init { + setEvent(NavBarContract.NavBarEvent.OnLoadUserData) + } + + override suspend fun handleEvent(event: NavBarContract.NavBarEvent) { + when (event) { + NavBarContract.NavBarEvent.OnLoadUserData -> loadUserName() + is NavBarContract.NavBarEvent.OnPageSelected -> updatePage(event.pageIndex) + } + } + + private fun updatePage(pageIndex: Int) { + setState { + copy(page = pageIndex) + } + } + + private fun loadUserName(){ + viewModelScope.launch { + try { + val userName = userInfoLocalDataSource.userName + val accessToken = userInfoLocalDataSource.accessToken + + setState { + copy( + userName = userName, + accessToken = accessToken, + ) + } + } catch (e: Exception){ + Log.e("NavBar", "Error loading user name: ${e.message}") + setSideEffect(NavBarContract.NavBarSideEffect.ShowErrorToast) + } + } + } + +} \ No newline at end of file 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..14acc60 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 @@ -8,25 +8,51 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import org.sopt.and.R @Composable fun NavIcon( navController: NavController, + navBarViewModel: NavBarViewModel = hiltViewModel(), route: String, modifier: Modifier = Modifier, icon: ImageVector, - text: String + text: String, + pageIndex: Int ){ + val uiState by navBarViewModel.uiState.collectAsStateWithLifecycle() + Column( - modifier = modifier.clickable {navController.navigate(route)}, + modifier = modifier.clickable { + navBarViewModel.setEvent(NavBarContract.NavBarEvent.OnPageSelected(pageIndex)) + when (route) { + "home" -> { + navController.navigate("home") { + popUpTo("home") { inclusive = true } + } + } + "search" -> { + navController.navigate("search") { + popUpTo("search") { inclusive = true } + } + } + "profile" -> { + navController.navigate("mypage") { + popUpTo("mypage") { inclusive = true } + } + } + } + }, 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..2f09c56 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 @@ -48,7 +49,7 @@ fun HomeLazyRow( contentPadding = PaddingValues(start = 8.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - items(images) { imageRes -> + items(items = images, key = { it }) { imageRes -> Image( modifier = Modifier .size(width.dp, height.dp) @@ -59,7 +60,5 @@ fun HomeLazyRow( ) } } - } - } 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..f20c07f 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,11 @@ fun MyPageProfileSection( ) Spacer(modifier = Modifier.width(10.dp)) Text( - "${deliveredEmail}님", + text = if (deliveredUserHobby != null) { + "${deliveredUserHobby}를 즐기는\n${deliveredUserName}님" + } else { + "취미를 아직 등록하지 않은 ${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..f394d09 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, @@ -57,7 +57,7 @@ fun SignUpTextField( text = buttonLabel, fontWeight = FontWeight.Bold, modifier = Modifier.clickable { - onPasswordVisibilityChange() // 클릭 시 비밀번호 가시성 토글 + onPasswordVisibilityChange() } ) } diff --git a/app/src/main/java/org/sopt/and/ui/components/TopBar/CustomIopAppBarSecond.kt b/app/src/main/java/org/sopt/and/ui/components/TopBar/CustomIopAppBarSecond.kt index 513c3ff..ad29459 100644 --- a/app/src/main/java/org/sopt/and/ui/components/TopBar/CustomIopAppBarSecond.kt +++ b/app/src/main/java/org/sopt/and/ui/components/TopBar/CustomIopAppBarSecond.kt @@ -19,7 +19,7 @@ import androidx.navigation.compose.rememberNavController import org.sopt.and.ui.components.SignUpandLogIn.SocialLoginSection @Composable -fun CustomTopAppBarSecond(navController: NavController){ +fun CustomTopAppBarSecond(){ TopAppBar( title = { }, colors = topAppBarColors( @@ -45,6 +45,5 @@ fun CustomTopAppBarSecond(navController: NavController){ @Preview(showBackground = true) @Composable fun AppBarSecondPreview(){ - val navController = rememberNavController() - CustomTopAppBarSecond(navController = navController) + CustomTopAppBarSecond() } \ No newline at end of file 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..c01bb00 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,12 +17,14 @@ 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 -fun CustomTopAppBar(navController: NavController){ +fun CustomTopAppBar(){ TopAppBar( title = { }, colors = topAppBarColors( @@ -55,4 +57,10 @@ fun CustomTopAppBar(navController: NavController){ ) } ) +} + +@Preview(showBackground = true) +@Composable +fun AppbarFirstPreivew(){ + CustomTopAppBar() } \ 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..52cdab3 --- /dev/null +++ b/app/src/main/java/org/sopt/and/util/base/BaseViewModel.kt @@ -0,0 +1,42 @@ +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() { + + private val initialState: State by lazy { createInitialState() } + abstract fun createInitialState(): State + + private val _uiState = MutableStateFlow(initialState) + val uiState: StateFlow get() = _uiState.asStateFlow() + val currentState: State get() = _uiState.value + + private val _event: MutableSharedFlow = MutableSharedFlow() + val event: SharedFlow get() = _event.asSharedFlow() + + private val _sideEffect : MutableSharedFlow = MutableSharedFlow() + val sideEffect: Flow get() = _sideEffect.asSharedFlow() + + //state 설정하는 부분, Event를 통해서만 State 변경 가능 + fun setState(reduce: State.() -> State) { + _uiState.value = currentState.reduce() + } + + //event 설정하는 부분 + open fun setEvent(event: Event) { + dispatchEvent(event) + } + private fun dispatchEvent(event: Event) = viewModelScope.launch { + handleEvent(event) + } + protected abstract suspend fun handleEvent(event: Event) + + //sideEffect 설정하는 부분 + protected fun setSideEffect(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..456a1e2 --- /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 \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 20e2a01..132244e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7da403f..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