diff --git a/.kotlin/sessions/kotlin-compiler-5458289260765828524.salive b/.kotlin/sessions/kotlin-compiler-5458289260765828524.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3cd67ca..4060245 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -5,6 +7,10 @@ plugins { alias(libs.plugins.kotlin.serialization) } +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) +} + android { namespace = "org.sopt.and" compileSdk = 34 @@ -17,6 +23,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BASE_URL", properties["base.url"].toString()) } buildTypes { @@ -37,11 +44,11 @@ android { } buildFeatures { compose = true + buildConfig = true } } dependencies { - implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -53,6 +60,15 @@ dependencies { implementation(libs.lifecycle.viewmodel.compose) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.compose.navigation) + implementation(libs.androidx.runtime.livedata) + // network + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization.converter) + + implementation(libs.androidx.datastore.preferences) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab506de..5cfda77 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + ( - WavveBottomNavigationItem( - label = R.string.bottom_navigation_home_label, - icon = Icons.Default.Home, - route = Routes.Home, - index = 0 - ), - WavveBottomNavigationItem( - label = R.string.bottom_navigation_search_label, - icon = Icons.Default.Search, - route = Routes.Search, - index = 1 - ), - WavveBottomNavigationItem( - label = R.string.bottom_navigation_my_info_label, - icon = Icons.Default.AccountCircle, - route = Routes.MyInfo(""), - index = 2 - ) - ) - val linkableSNS = listOf>( Pair(R.drawable.kakao_talk_icon, R.string.link_kakao_icon_description), Pair(R.drawable.t_world_icon, R.string.link_tworld_icon_description), @@ -48,4 +28,27 @@ object WavveUtils { fun transformationPasswordVisual(isVisible: Boolean): VisualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation() + + fun showToast( + context: Context, + @StringRes message: Int + ) = Toast.makeText( + context, + context.getString(message), + Toast.LENGTH_SHORT + ).show() + + fun showSnackbar( + scope: CoroutineScope, + context: Context, + snackbarHostState: SnackbarHostState, + @StringRes message: Int + ) = scope.launch { + snackbarHostState.showSnackbar( + message = getString( + context, + message + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/components/SignInOrSignUpTextField.kt b/app/src/main/java/org/sopt/and/components/SignInOrSignUpTextField.kt index 3025e7d..b196848 100644 --- a/app/src/main/java/org/sopt/and/components/SignInOrSignUpTextField.kt +++ b/app/src/main/java/org/sopt/and/components/SignInOrSignUpTextField.kt @@ -16,14 +16,14 @@ import org.sopt.and.ui.theme.Grey200 @Composable fun SignInOrSignUpTextField( - emailOrPassword: String, + information: String, onValueChange: (String) -> Unit, placeholder: Int, visualTransformation: VisualTransformation = VisualTransformation.None, trailingIcon: @Composable (() -> Unit)? = null ) { TextField( - value = emailOrPassword, + value = information, onValueChange = onValueChange, modifier = Modifier.fillMaxWidth(), colors = TextFieldDefaults.colors( diff --git a/app/src/main/java/org/sopt/and/home/HomeScreen.kt b/app/src/main/java/org/sopt/and/home/HomeScreen.kt index 4ab25c8..59529fd 100644 --- a/app/src/main/java/org/sopt/and/home/HomeScreen.kt +++ b/app/src/main/java/org/sopt/and/home/HomeScreen.kt @@ -8,16 +8,16 @@ 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.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import org.sopt.and.R import org.sopt.and.home.components.HomeBannerPager @@ -31,10 +31,8 @@ import org.sopt.and.ui.theme.Grey100 fun HomeScreen( innerPadding: PaddingValues ) { - val scrollState = rememberScrollState() - val homeViewModel = viewModel() - val homeUiState by homeViewModel.uiState.collectAsState() + val homeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() Column( modifier = Modifier @@ -44,23 +42,32 @@ fun HomeScreen( ) { HomeTopBar(genres = homeUiState.genres) - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(scrollState) + LazyColumn( + modifier = Modifier.fillMaxWidth(), + state = rememberLazyListState() ) { - HomeBannerPager(homeUiState.banners) + item { + HomeBannerPager(homeUiState.banners) + } - Spacer(modifier = Modifier.height(20.dp)) + item { + Spacer(modifier = Modifier.height(20.dp)) + } - RecommendList( - title = stringResource(R.string.home_picks_of_editor_title), - items = homeUiState.recommends - ) + item { + RecommendList( + title = stringResource(R.string.home_picks_of_editor_title), + items = homeUiState.recommends + ) + } - Spacer(modifier = Modifier.height(20.dp)) + item { + Spacer(modifier = Modifier.height(20.dp)) + } - Top20List(homeUiState.rankers) + item { + Top20List(homeUiState.rankers) + } } HomeBottomCoupon() diff --git a/app/src/main/java/org/sopt/and/home/components/HomeTopBar.kt b/app/src/main/java/org/sopt/and/home/components/HomeTopBar.kt index c3adfd3..0936f9f 100644 --- a/app/src/main/java/org/sopt/and/home/components/HomeTopBar.kt +++ b/app/src/main/java/org/sopt/and/home/components/HomeTopBar.kt @@ -11,6 +11,7 @@ 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.lazy.LazyRow import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -63,15 +64,18 @@ fun HomeTopBar( ) } - Row( + LazyRow( modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp), horizontalArrangement = Arrangement.spacedBy(14.dp) ) { - genres.forEach { genre -> + items( + count = genres.size, + key = { genres[it] } + ) { index -> Text( - text = stringResource(genre), + text = stringResource(genres[index]), color = Grey200, fontSize = 14.sp ) diff --git a/app/src/main/java/org/sopt/and/myinfo/MyInfoScreen.kt b/app/src/main/java/org/sopt/and/myinfo/MyInfoScreen.kt index 6100036..3ecabb9 100644 --- a/app/src/main/java/org/sopt/and/myinfo/MyInfoScreen.kt +++ b/app/src/main/java/org/sopt/and/myinfo/MyInfoScreen.kt @@ -23,7 +23,8 @@ import org.sopt.and.ui.theme.Black100 @Composable fun MyInfoScreen( paddingValues: PaddingValues, - myEmail: String, + myInfoViewModel: MyInfoViewModel, + myInfoUiState: MyInfoUiState, modifier: Modifier = Modifier ) { Column( @@ -33,7 +34,8 @@ fun MyInfoScreen( .padding(paddingValues) ) { MyInfoProfile( - myEmail = myEmail, + myInfoViewModel = myInfoViewModel, + myInfoUiState = myInfoUiState, modifier = Modifier.weight(0.16f) ) @@ -73,7 +75,8 @@ fun MyScreenPreview() { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MyInfoScreen( paddingValues = innerPadding, - myEmail = "" + myInfoViewModel = TODO(), + myInfoUiState = TODO() ) } } diff --git a/app/src/main/java/org/sopt/and/myinfo/MyInfoUiState.kt b/app/src/main/java/org/sopt/and/myinfo/MyInfoUiState.kt index f06f59e..2e2babd 100644 --- a/app/src/main/java/org/sopt/and/myinfo/MyInfoUiState.kt +++ b/app/src/main/java/org/sopt/and/myinfo/MyInfoUiState.kt @@ -1,5 +1,5 @@ package org.sopt.and.myinfo data class MyInfoUiState( - val myEmail: String = "" + val myHobby: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/myinfo/MyInfoViewModel.kt b/app/src/main/java/org/sopt/and/myinfo/MyInfoViewModel.kt index 83994d3..2ee120e 100644 --- a/app/src/main/java/org/sopt/and/myinfo/MyInfoViewModel.kt +++ b/app/src/main/java/org/sopt/and/myinfo/MyInfoViewModel.kt @@ -1,16 +1,44 @@ package org.sopt.and.myinfo -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import org.sopt.and.myinfo.dto.GetHobbyResponseDto +import org.sopt.and.services.ServicePool +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MyInfoViewModel(application: Application) : AndroidViewModel(application) { + private val userService by lazy { ServicePool.userService(application) } -class MyInfoViewModel( -) : ViewModel() { private val _uiState = MutableStateFlow(MyInfoUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun setMyEmail(email: String) { - _uiState.value = _uiState.value.copy(myEmail = email) + fun setMyHobby(myHobby: String) { + _uiState.value = _uiState.value.copy(myHobby = myHobby) + } + + fun getMyHobby() { + userService.getMyHobby().enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + response.body()?.result?.hobby?.let { setMyHobby(it) } + } else { + setMyHobby("오류") + } + } + + override fun onFailure(call: Call, t: Throwable) { + // 어떤 처리를 할까요? + } + } + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/myinfo/components/MyInfoProfile.kt b/app/src/main/java/org/sopt/and/myinfo/components/MyInfoProfile.kt index 9f28764..e0e7c1f 100644 --- a/app/src/main/java/org/sopt/and/myinfo/components/MyInfoProfile.kt +++ b/app/src/main/java/org/sopt/and/myinfo/components/MyInfoProfile.kt @@ -19,14 +19,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import org.sopt.and.R +import org.sopt.and.myinfo.MyInfoUiState +import org.sopt.and.myinfo.MyInfoViewModel import org.sopt.and.ui.theme.Grey100 import org.sopt.and.ui.theme.White100 @Composable fun MyInfoProfile( - myEmail: String, + myInfoViewModel: MyInfoViewModel, + myInfoUiState: MyInfoUiState, modifier: Modifier = Modifier ) { + myInfoViewModel.getMyHobby() + Row( modifier = modifier .fillMaxWidth() @@ -43,8 +48,8 @@ fun MyInfoProfile( Spacer(modifier = Modifier.width(5.dp)) - Text( - text = myEmail, + Text( // 만약에 네트워크 통신이 늦었을 때 얘가 recomposition이 되나요?? + text = if (myInfoUiState.myHobby.isNotEmpty()) myInfoUiState.myHobby else "Loading...", color = White100 ) diff --git a/app/src/main/java/org/sopt/and/myinfo/dto/GetHobbyResponseDto.kt b/app/src/main/java/org/sopt/and/myinfo/dto/GetHobbyResponseDto.kt new file mode 100644 index 0000000..06d99d9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/myinfo/dto/GetHobbyResponseDto.kt @@ -0,0 +1,14 @@ +package org.sopt.and.myinfo.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class GetHobbyResponseDto( + val result: GetHobbyResponseResultDto? = null, + val code: String? = null +) + +@Serializable +data class GetHobbyResponseResultDto( + val hobby: String +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/navigation/Navigation.kt b/app/src/main/java/org/sopt/and/navigation/Navigation.kt index af71241..61ee50d 100644 --- a/app/src/main/java/org/sopt/and/navigation/Navigation.kt +++ b/app/src/main/java/org/sopt/and/navigation/Navigation.kt @@ -3,15 +3,14 @@ package org.sopt.and.navigation import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import org.sopt.and.WavveUtils +import androidx.navigation.navOptions import org.sopt.and.home.HomeScreen import org.sopt.and.myinfo.MyInfoScreen import org.sopt.and.myinfo.MyInfoViewModel @@ -23,58 +22,59 @@ import org.sopt.and.signup.SignUpScreen fun Navigation( ) { val navigationViewModel = viewModel() - val navigationUiState by navigationViewModel.uiState.collectAsState() + val navigationUiState by navigationViewModel.uiState.collectAsStateWithLifecycle() val navController = rememberNavController() val myInfoViewModel = viewModel() - val myInfoUiState by myInfoViewModel.uiState.collectAsState() + val myInfoUiState by myInfoViewModel.uiState.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { if (navigationUiState.isBottomNavigationVisible) { WavveBottomNavigation( - items = WavveUtils.wavveBottomNavigationItems, - navController, - navigationViewModel::setNavigationSelectedIndex, - navigationUiState.navigationSelectedIndex + items = navigationUiState.wavveBottomNavigationItems, + navController = navController, + setNavigationSelectedScreenIndex = navigationViewModel::setNavigationSelectedIndex, + navigationSelectedScreenIndex = navigationUiState.navigationSelectedIndex ) } } ) { innerPadding -> NavHost( navController = navController, - startDestination = Routes.SignIn("", "") // 이녀석 생성자 안써서 3시간 날림 + startDestination = Routes.SignIn // 이녀석 생성자 안써서 3시간 날림 ) { composable { SignInScreen( - navigateToSignUp = { - navController.navigate(Routes.SignUp) - }, - navigateToMyInfo = { myEmail -> + navigateToSignUp = { navController.navigate(route = Routes.SignUp) }, + navigateToMyInfo = { navigationViewModel.changeBottomNavigationVisibility() - navController.navigate( - Routes.MyInfo(myEmail) - ) + navController.navigate(Routes.MyInfo) } ) } composable { SignUpScreen( - navigateToSignIn = { signUpEmail, signUpPassword -> - navController.navigate(Routes.SignIn(signUpEmail, signUpPassword)) + navigateToSignIn = { + navController.navigate( + route = Routes.SignIn, + navOptions = navOptions { + popUpTo { + inclusive = true + } + } + ) } ) } - composable { backStackEntry -> - val item = backStackEntry.toRoute() - myInfoViewModel.setMyEmail(item.myEmail) - + composable { MyInfoScreen( paddingValues = innerPadding, - myInfoUiState.myEmail + myInfoViewModel = myInfoViewModel, + myInfoUiState = myInfoUiState ) } diff --git a/app/src/main/java/org/sopt/and/navigation/NavigationUiState.kt b/app/src/main/java/org/sopt/and/navigation/NavigationUiState.kt index 7710c9f..afb5b1a 100644 --- a/app/src/main/java/org/sopt/and/navigation/NavigationUiState.kt +++ b/app/src/main/java/org/sopt/and/navigation/NavigationUiState.kt @@ -1,8 +1,33 @@ package org.sopt.and.navigation +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Search +import org.sopt.and.R import org.sopt.and.WavveUtils data class NavigationUiState( val isBottomNavigationVisible: Boolean = false, - val navigationSelectedIndex: Int = WavveUtils.MYINFO_SCREEN_INDEX + val navigationSelectedIndex: Int = WavveUtils.MYINFO_SCREEN_INDEX, + val wavveBottomNavigationItems: List = listOf( + WavveBottomNavigationItem( + label = R.string.bottom_navigation_home_label, + icon = Icons.Default.Home, + route = Routes.Home, + index = 0 + ), + WavveBottomNavigationItem( + label = R.string.bottom_navigation_search_label, + icon = Icons.Default.Search, + route = Routes.Search, + index = 1 + ), + WavveBottomNavigationItem( + label = R.string.bottom_navigation_my_info_label, + icon = Icons.Default.AccountCircle, + route = Routes.MyInfo, + index = 2 + ) + ) ) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/navigation/Routes.kt b/app/src/main/java/org/sopt/and/navigation/Routes.kt index 90b3f77..6b318c7 100644 --- a/app/src/main/java/org/sopt/and/navigation/Routes.kt +++ b/app/src/main/java/org/sopt/and/navigation/Routes.kt @@ -2,25 +2,19 @@ package org.sopt.and.navigation import kotlinx.serialization.Serializable - -object Routes { +sealed class Routes { @Serializable - data class MyInfo( - val myEmail: String - ) + object MyInfo : Routes() @Serializable - data class SignIn( - val signUpEmail: String, - val signUpPassword: String - ) + object SignIn : Routes() @Serializable - object SignUp + object SignUp : Routes() @Serializable - object Home + object Home : Routes() @Serializable - object Search + object Search : Routes() } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigation.kt b/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigation.kt index 6861781..eedfc81 100644 --- a/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigation.kt +++ b/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigation.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import org.sopt.and.WavveUtils import org.sopt.and.ui.theme.Grey100 import org.sopt.and.ui.theme.Grey200 @@ -77,7 +76,7 @@ fun WavveBottomNavigation( @Composable fun WavveBottomNavigationPreview() { WavveBottomNavigation( - WavveUtils.wavveBottomNavigationItems, + listOf(), navController = rememberNavController(), setNavigationSelectedScreenIndex = TODO(), navigationSelectedScreenIndex = TODO(), diff --git a/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigationItem.kt b/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigationItem.kt index 56f8a04..81b4aae 100644 --- a/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigationItem.kt +++ b/app/src/main/java/org/sopt/and/navigation/WavveBottomNavigationItem.kt @@ -5,6 +5,6 @@ import androidx.compose.ui.graphics.vector.ImageVector data class WavveBottomNavigationItem( val label: Int, val icon: ImageVector, - val route: Any, + val route: Routes, val index: Int ) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/services/ApiFactory.kt b/app/src/main/java/org/sopt/and/services/ApiFactory.kt new file mode 100644 index 0000000..4c62fe5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/services/ApiFactory.kt @@ -0,0 +1,38 @@ +package org.sopt.and.services + +import android.content.Context +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.sopt.and.BuildConfig +import retrofit2.Retrofit + +object ApiFactory { + private const val BASE_URL: String = BuildConfig.BASE_URL + + fun createRetrofit(context: Context): Retrofit { + val authInterceptor = AuthInterceptor(context) + + val client = OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + inline fun create(context: Context): T = + createRetrofit(context).create(T::class.java) +} + +object ServicePool { + fun userService(context: Context) = ApiFactory.create(context) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/services/AuthInterceptor.kt b/app/src/main/java/org/sopt/and/services/AuthInterceptor.kt new file mode 100644 index 0000000..a3131e4 --- /dev/null +++ b/app/src/main/java/org/sopt/and/services/AuthInterceptor.kt @@ -0,0 +1,28 @@ +package org.sopt.and.services + +import android.content.Context +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(context: Context) : Interceptor { + private val tokenManager = TokenManager(context) + + override fun intercept(chain: Interceptor.Chain): Response { + var token: String? + + runBlocking { + token = tokenManager.getToken().first() + } + + val request = chain.request().newBuilder() + .apply { + if (token != null) { + addHeader("token", token!!) + } + } + .build() + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/services/TokenManager.kt b/app/src/main/java/org/sopt/and/services/TokenManager.kt new file mode 100644 index 0000000..5d5f109 --- /dev/null +++ b/app/src/main/java/org/sopt/and/services/TokenManager.kt @@ -0,0 +1,29 @@ +package org.sopt.and.services + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + + +// token을 저장할 방법으로 공식문서가 권장하는 preferenceDatastore 채택 + +val Context.dataStore by preferencesDataStore("token") + +class TokenManager(private val context: Context) { + private val TOKEN_KEY = stringPreferencesKey("auth_token") + + fun getToken(): Flow { + return context.dataStore.data.map { preferences -> + preferences[TOKEN_KEY] + } + } + + suspend fun saveToken(token: String) { + context.dataStore.edit { preferences -> + preferences[TOKEN_KEY] = token + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/services/UserService.kt b/app/src/main/java/org/sopt/and/services/UserService.kt new file mode 100644 index 0000000..f430294 --- /dev/null +++ b/app/src/main/java/org/sopt/and/services/UserService.kt @@ -0,0 +1,22 @@ +package org.sopt.and.services + +import org.sopt.and.myinfo.dto.GetHobbyResponseDto +import org.sopt.and.signin.dto.SignInRequestDto +import org.sopt.and.signin.dto.SignInResponseDto +import org.sopt.and.signup.dto.SignUpRequestDto +import org.sopt.and.signup.dto.SignUpResponseDto +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface UserService { + @POST("/user") + fun signUp(@Body request: SignUpRequestDto): Call + + @POST("/login") + fun signIn(@Body request: SignInRequestDto): Call + + @GET("/user/my-hobby") + fun getMyHobby(): Call +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signin/SignInScreen.kt b/app/src/main/java/org/sopt/and/signin/SignInScreen.kt index 0591e41..0a4551a 100644 --- a/app/src/main/java/org/sopt/and/signin/SignInScreen.kt +++ b/app/src/main/java/org/sopt/and/signin/SignInScreen.kt @@ -11,45 +11,43 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import org.sopt.and.R import org.sopt.and.components.LinkWithSNSBox -import org.sopt.and.signin.components.* -import org.sopt.and.ui.theme.* +import org.sopt.and.signin.components.SignInBtn +import org.sopt.and.signin.components.SignInPasswordField +import org.sopt.and.signin.components.SignInToAdditionalFeatures +import org.sopt.and.signin.components.SignInTopBar +import org.sopt.and.signin.components.SignInUsernameField import org.sopt.and.ui.theme.ANDANDROIDTheme +import org.sopt.and.ui.theme.Black100 @Composable fun SignInScreen( navigateToSignUp: () -> Unit, - navigateToMyInfo: (String) -> Unit, + navigateToMyInfo: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() - val context = LocalContext.current val signInViewModel = viewModel() - val signInUiState by signInViewModel.uiState.collectAsState() + val signInUiState by signInViewModel.uiState.collectAsStateWithLifecycle() - val signInEmail = signInUiState.signInEmail + val signInUsername = signInUiState.signInUsername val signInPassword = signInUiState.signInPassword val isSignInPasswordVisible = signInUiState.isSignInPasswordVisible Scaffold( modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { innerPadding -> Column( modifier = modifier @@ -67,9 +65,9 @@ fun SignInScreen( Spacer(modifier = Modifier.height(60.dp)) - SignInEmailField( - signInEmail = signInEmail, - onSignInEmailChange = signInViewModel::setSignInEmail + SignInUsernameField( + signInUsername = signInUsername, + onSignInUsernameChange = signInViewModel::setSignInUsername ) Spacer(modifier = Modifier.height(5.dp)) @@ -84,12 +82,9 @@ fun SignInScreen( Spacer(modifier = Modifier.height(30.dp)) SignInBtn( - isLoginSuccess = signInViewModel::isLoginSuccess, - scope = scope, - context = context, snackbarHostState = snackbarHostState, navigateToMyInfo = navigateToMyInfo, - signInEmail = signInUiState.signInEmail + signInViewModel = signInViewModel ) Spacer(modifier = Modifier.height(20.dp)) @@ -115,7 +110,7 @@ fun SignInScreenPreview() { innerPadding SignInScreen( navigateToSignUp = {}, - navigateToMyInfo = { a -> } + navigateToMyInfo = { -> } ) } } diff --git a/app/src/main/java/org/sopt/and/signin/SignInUiState.kt b/app/src/main/java/org/sopt/and/signin/SignInUiState.kt index e0ec1ff..d0bc132 100644 --- a/app/src/main/java/org/sopt/and/signin/SignInUiState.kt +++ b/app/src/main/java/org/sopt/and/signin/SignInUiState.kt @@ -1,7 +1,14 @@ package org.sopt.and.signin data class SignInUiState( - val signInEmail: String = "", + val signInUsername: String = "", val signInPassword: String = "", val isSignInPasswordVisible: Boolean = false -) \ No newline at end of file +) + +sealed class SignInResult { + object Initial : SignInResult() + object Success : SignInResult() + object FailurePasswordLength : SignInResult() + object FailureWrongPassword : SignInResult() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signin/SignInViewModel.kt b/app/src/main/java/org/sopt/and/signin/SignInViewModel.kt index f1dc9d8..1e29c65 100644 --- a/app/src/main/java/org/sopt/and/signin/SignInViewModel.kt +++ b/app/src/main/java/org/sopt/and/signin/SignInViewModel.kt @@ -1,24 +1,45 @@ package org.sopt.and.signin -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.navigation.toRoute +import android.app.Application +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import org.sopt.and.navigation.Routes +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.sopt.and.services.ServicePool +import org.sopt.and.services.TokenManager +import org.sopt.and.signin.dto.SignInRequestDto +import org.sopt.and.signin.dto.SignInResponseDto +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class SignInViewModel(application: Application) : AndroidViewModel(application) { + private val userService by lazy { ServicePool.userService(application) } + private val tokenManager = TokenManager(application) -class SignInViewModel( - savedStateHandle: SavedStateHandle -) : ViewModel() { private val _uiState = MutableStateFlow(SignInUiState()) val uiState: StateFlow = _uiState.asStateFlow() - val signUpAccount = savedStateHandle.toRoute() + private val _signInResult = MutableLiveData() + val signInResult: LiveData = _signInResult + + private val _signInResultState = mutableStateOf(null) + val signInResultState: State get() = _signInResultState - fun setSignInEmail(signInEmail: String) { + fun initSignInResult() { + _signInResult.value = SignInResult.Initial + } + + fun setSignInUsername(signInUsername: String) { _uiState.value = _uiState.value.copy( - signInEmail = signInEmail + signInUsername = signInUsername ) } @@ -34,8 +55,46 @@ class SignInViewModel( ) } - fun isLoginSuccess(): Boolean = - signUpAccount.signUpEmail.isNotEmpty() - && _uiState.value.signInEmail == signUpAccount.signUpEmail - && _uiState.value.signInPassword == signUpAccount.signUpPassword + fun signIn( + signInUsername: String, + signInPassword: String + ) { + userService.signIn( + request = SignInRequestDto( + username = signInUsername, + password = signInPassword + ) + ).enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + _signInResultState.value = response.body() + _signInResult.value = SignInResult.Success + + response.body()?.result?.token?.let { token -> + viewModelScope.launch { + tokenManager.saveToken(token) + } + } + } else { + _signInResultState.value = response.errorBody()?.string() + ?.let { Json.decodeFromString(it) } + + if (signInResultState.value?.code == "01" && response.code() == 400) { + _signInResult.value = SignInResult.FailurePasswordLength + } else if (signInResultState.value?.code == "01" && response.code() == 403) { + _signInResult.value = SignInResult.FailureWrongPassword + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + // 어떤 구현이 들어갈까요? + } + } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signin/components/SignInBtn.kt b/app/src/main/java/org/sopt/and/signin/components/SignInBtn.kt index 5f99103..cc6e709 100644 --- a/app/src/main/java/org/sopt/and/signin/components/SignInBtn.kt +++ b/app/src/main/java/org/sopt/and/signin/components/SignInBtn.kt @@ -1,6 +1,5 @@ package org.sopt.and.signin.components -import android.content.Context import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,50 +8,78 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat.getString -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import org.sopt.and.R +import org.sopt.and.WavveUtils +import org.sopt.and.signin.SignInResult +import org.sopt.and.signin.SignInViewModel import org.sopt.and.ui.theme.Blue100 import org.sopt.and.ui.theme.White100 @Composable fun SignInBtn( - isLoginSuccess: () -> Boolean, - scope: CoroutineScope, - context: Context, snackbarHostState: SnackbarHostState, - navigateToMyInfo: (String) -> Unit, - signInEmail: String + navigateToMyInfo: () -> Unit, + signInViewModel: SignInViewModel ) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val signInResult by signInViewModel.signInResult.observeAsState() + + LaunchedEffect(signInResult) { + when (signInResult) { + is SignInResult.Success -> { + WavveUtils.showSnackbar( + scope = scope, + context = context, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_success_message, + ) + navigateToMyInfo() + signInViewModel.initSignInResult() + } + + is SignInResult.FailurePasswordLength -> { + WavveUtils.showSnackbar( + scope = scope, + context = context, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_failed_password_length, + ) + signInViewModel.initSignInResult() + } + + is SignInResult.FailureWrongPassword -> { + WavveUtils.showSnackbar( + scope = scope, + context = context, + snackbarHostState = snackbarHostState, + message = R.string.sign_in_failed_wrong_password, + ) + signInViewModel.initSignInResult() + } + + else -> {} + } + } + Button( modifier = Modifier .fillMaxWidth() .height(50.dp), onClick = { - if (isLoginSuccess()) { - scope.launch { - snackbarHostState.showSnackbar( - message = getString( - context, - R.string.sign_in_success_message - ) - ) - navigateToMyInfo(signInEmail) - } - } else { - scope.launch { - snackbarHostState.showSnackbar( - message = getString( - context, - R.string.sign_in_failed_message - ) - ) - } - } + signInViewModel.signIn( + signInUsername = signInViewModel.uiState.value.signInUsername, + signInPassword = signInViewModel.uiState.value.signInPassword + ) }, shape = RoundedCornerShape(50.dp), colors = ButtonDefaults.buttonColors( diff --git a/app/src/main/java/org/sopt/and/signin/components/SignInEmailField.kt b/app/src/main/java/org/sopt/and/signin/components/SignInEmailField.kt deleted file mode 100644 index f284c75..0000000 --- a/app/src/main/java/org/sopt/and/signin/components/SignInEmailField.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.sopt.and.signin.components - -import androidx.compose.runtime.Composable -import org.sopt.and.R -import org.sopt.and.components.SignInOrSignUpTextField - -@Composable -fun SignInEmailField( - signInEmail: String, - onSignInEmailChange: (String) -> Unit -) { - SignInOrSignUpTextField( - emailOrPassword = signInEmail, - onValueChange = onSignInEmailChange, - placeholder = R.string.sign_in_email_placeholder - ) -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signin/components/SignInPasswordField.kt b/app/src/main/java/org/sopt/and/signin/components/SignInPasswordField.kt index f2ca806..b13588a 100644 --- a/app/src/main/java/org/sopt/and/signin/components/SignInPasswordField.kt +++ b/app/src/main/java/org/sopt/and/signin/components/SignInPasswordField.kt @@ -14,7 +14,7 @@ fun SignInPasswordField( onVisibilityChange: () -> Unit ) { SignInOrSignUpTextField( - emailOrPassword = signInPassword, + information = signInPassword, onValueChange = onSignInPasswordChange, placeholder = R.string.sign_in_password_placeholder, visualTransformation = transformationPasswordVisual(isSignInPasswordVisible), diff --git a/app/src/main/java/org/sopt/and/signin/components/SignInUsernameField.kt b/app/src/main/java/org/sopt/and/signin/components/SignInUsernameField.kt new file mode 100644 index 0000000..e35edec --- /dev/null +++ b/app/src/main/java/org/sopt/and/signin/components/SignInUsernameField.kt @@ -0,0 +1,17 @@ +package org.sopt.and.signin.components + +import androidx.compose.runtime.Composable +import org.sopt.and.R +import org.sopt.and.components.SignInOrSignUpTextField + +@Composable +fun SignInUsernameField( + signInUsername: String, + onSignInUsernameChange: (String) -> Unit +) { + SignInOrSignUpTextField( + information = signInUsername, + onValueChange = onSignInUsernameChange, + placeholder = R.string.sign_in_username_placeholder + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signin/dto/SignInRequestDto.kt b/app/src/main/java/org/sopt/and/signin/dto/SignInRequestDto.kt new file mode 100644 index 0000000..45f53bc --- /dev/null +++ b/app/src/main/java/org/sopt/and/signin/dto/SignInRequestDto.kt @@ -0,0 +1,9 @@ +package org.sopt.and.signin.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SignInRequestDto( + val username: String, + val password: String +) diff --git a/app/src/main/java/org/sopt/and/signin/dto/SignInResponseDto.kt b/app/src/main/java/org/sopt/and/signin/dto/SignInResponseDto.kt new file mode 100644 index 0000000..940ea0f --- /dev/null +++ b/app/src/main/java/org/sopt/and/signin/dto/SignInResponseDto.kt @@ -0,0 +1,14 @@ +package org.sopt.and.signin.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SignInResponseDto( + val result: SignInResponseResultDto? = null, + val code: String? = null +) + +@Serializable +data class SignInResponseResultDto( + val token: String +) diff --git a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt index 38733f5..4ec87a3 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpScreen.kt @@ -9,33 +9,31 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import org.sopt.and.R import org.sopt.and.components.LinkWithSNSBox -import org.sopt.and.signup.components.* -import org.sopt.and.ui.theme.* +import org.sopt.and.signup.components.SignUpBtn +import org.sopt.and.signup.components.SignUpGreetingText +import org.sopt.and.signup.components.SignUpHobbyField +import org.sopt.and.signup.components.SignUpPasswordField +import org.sopt.and.signup.components.SignUpTopBar +import org.sopt.and.signup.components.SignUpUsernameField import org.sopt.and.ui.theme.ANDANDROIDTheme +import org.sopt.and.ui.theme.Black100 @Composable fun SignUpScreen( modifier: Modifier = Modifier, - navigateToSignIn: (String, String) -> Unit, + navigateToSignIn: () -> Unit, ) { - val context = LocalContext.current - val signUpViewModel = viewModel() - val signUpUiState by signUpViewModel.uiState.collectAsState() - - val signUpEmail = signUpUiState.signUpEmail - val signUpPassword = signUpUiState.signUpPassword - val isSignUpPasswordVisible = signUpUiState.isSignUpPasswordVisible + val signUpUiState by signUpViewModel.uiState.collectAsStateWithLifecycle() Column( modifier = modifier @@ -52,35 +50,42 @@ fun SignUpScreen( Spacer(modifier = Modifier.height(30.dp)) - SignUpGreetingText(24, context) + SignUpGreetingText(fontSize = 24) Spacer(modifier = Modifier.height(20.dp)) - SignUpEmailField( - email = signUpEmail, - onSignUpEmailChange = signUpViewModel::setSignUpEmail + SignUpUsernameField( + signUpUsername = signUpUiState.signUpUsername, + onSignUpUsernameChange = signUpViewModel::setSignUpUsername ) Spacer(modifier = Modifier.height(20.dp)) SignUpPasswordField( - signUpPassword = signUpPassword, + signUpPassword = signUpUiState.signUpPassword, onSignUpPasswordChange = signUpViewModel::setSignUpPassword, - isSignUpPasswordVisible = isSignUpPasswordVisible, + isSignUpPasswordVisible = signUpUiState.isSignUpPasswordVisible, onVisibilityChange = signUpViewModel::changeSignUpPasswordVisibility ) + Spacer(modifier = Modifier.height(20.dp)) + + SignUpHobbyField( + signUpHobby = signUpUiState.signUpHobby, + onSignUpHobbyChange = signUpViewModel::setSignUpHobby + ) + Spacer(modifier = Modifier.size(40.dp)) LinkWithSNSBox(stringResource(R.string.sign_in_link_with_another_service_title)) } SignUpBtn( - signUpEmail = signUpEmail, - signUpPassword = signUpPassword, - context = context, + signUpUsername = signUpUiState.signUpUsername, + signUpPassword = signUpUiState.signUpPassword, + signUpHobby = signUpUiState.signUpHobby, onSignUpComplete = navigateToSignIn, - signUpViewModel + signUpViewModel = signUpViewModel ) } } @@ -99,7 +104,7 @@ fun SignUpScreenPreview() { SignUpScreen( modifier = Modifier .padding(innerPadding), - navigateToSignIn = { email, password -> } + navigateToSignIn = { } ) } } diff --git a/app/src/main/java/org/sopt/and/signup/SignUpUiState.kt b/app/src/main/java/org/sopt/and/signup/SignUpUiState.kt index 905a3fb..27e3742 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpUiState.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpUiState.kt @@ -1,7 +1,15 @@ package org.sopt.and.signup data class SignUpUiState( - val signUpEmail: String = "", + val signUpUsername: String = "", val signUpPassword: String = "", + val signUpHobby: String = "", val isSignUpPasswordVisible: Boolean = false -) \ No newline at end of file +) + +sealed class SignUpResult { + object Initial : SignUpResult() + object Success : SignUpResult() + object FailureInformationLength : SignUpResult() + object FailureDuplicateUsername : SignUpResult() +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/signup/SignUpViewModel.kt index f02a875..c22134b 100644 --- a/app/src/main/java/org/sopt/and/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/sopt/and/signup/SignUpViewModel.kt @@ -1,19 +1,42 @@ package org.sopt.and.signup -import androidx.core.util.PatternsCompat -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import org.sopt.and.WavveUtils +import kotlinx.serialization.json.Json +import org.sopt.and.services.ServicePool +import org.sopt.and.signup.dto.SignUpRequestDto +import org.sopt.and.signup.dto.SignUpResponseDto +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class SignUpViewModel(application: Application) : AndroidViewModel(application) { + private val userService by lazy { ServicePool.userService(application) } + + private val _signUpResultState = mutableStateOf(null) + val signUpResultState: State get() = _signUpResultState -class SignUpViewModel : ViewModel() { private val _uiState = MutableStateFlow(SignUpUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun setSignUpEmail(signUpEmail: String) { + private val _signUpResult = MutableLiveData() + val signUpResult: LiveData = _signUpResult + + fun initSignUpResult() { + _signUpResult.value = SignUpResult.Initial + } + + fun setSignUpUsername(signUpUsername: String) { _uiState.value = _uiState.value.copy( - signUpEmail = signUpEmail + signUpUsername = signUpUsername ) } @@ -23,28 +46,53 @@ class SignUpViewModel : ViewModel() { ) } + fun setSignUpHobby(signUpHobby: String) { + _uiState.value = _uiState.value.copy( + signUpHobby = signUpHobby + ) + } + fun changeSignUpPasswordVisibility() { _uiState.value = _uiState.value.copy( isSignUpPasswordVisible = !_uiState.value.isSignUpPasswordVisible ) } - fun validateSignUpPassword(signUpPassword: String): Boolean { - if (signUpPassword.length !in WavveUtils.MIN_PASSWORD_LENGTH..WavveUtils.MAX_PASSWORD_LENGTH) return false + fun signUp( + signUpUsername: String, + signUpPassword: String, + signUpHobby: String + ) { + userService.signUp( + request = SignUpRequestDto( + username = signUpUsername, + password = signUpPassword, + hobby = signUpHobby + ) + ).enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + _signUpResultState.value = response.body() + _signUpResult.value = SignUpResult.Success + } else { + _signUpResultState.value = response.errorBody()?.string() + ?.let { Json.decodeFromString(it) } + if (signUpResultState.value?.code == "01" && response.code() == 400) { + _signUpResult.value = SignUpResult.FailureInformationLength + } else if (signUpResultState.value?.code == "00" && response.code() == 409) { + _signUpResult.value = SignUpResult.FailureDuplicateUsername + } + } + } - val validateValues = listOf( - signUpPassword.any { it.isLowerCase() }, - signUpPassword.any { it.isUpperCase() }, - signUpPassword.any { it.isDigit() }, - signUpPassword.any { !it.isLetterOrDigit() } + override fun onFailure(call: Call, t: Throwable) { + // 어떤 처리를 할까요? + } + } ) - val isValidate = validateValues.count { it } >= 3 - - return isValidate } - - fun validateSignUpEmail(email: String): Boolean = PatternsCompat - .EMAIL_ADDRESS - .matcher(email) - .matches() } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/components/SignUpBtn.kt b/app/src/main/java/org/sopt/and/signup/components/SignUpBtn.kt index b14f918..2d8bcc9 100644 --- a/app/src/main/java/org/sopt/and/signup/components/SignUpBtn.kt +++ b/app/src/main/java/org/sopt/and/signup/components/SignUpBtn.kt @@ -1,7 +1,5 @@ package org.sopt.and.signup.components -import android.content.Context -import android.widget.Toast import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,49 +7,73 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.R +import org.sopt.and.WavveUtils +import org.sopt.and.signup.SignUpResult.FailureDuplicateUsername +import org.sopt.and.signup.SignUpResult.FailureInformationLength +import org.sopt.and.signup.SignUpResult.Success import org.sopt.and.signup.SignUpViewModel import org.sopt.and.ui.theme.Grey200 import org.sopt.and.ui.theme.White100 @Composable fun SignUpBtn( - signUpEmail: String, + signUpUsername: String, signUpPassword: String, - context: Context, - onSignUpComplete: (String, String) -> Unit, + signUpHobby: String, + onSignUpComplete: () -> Unit, signUpViewModel: SignUpViewModel ) { - Button( - onClick = { - val isEmailValid = signUpViewModel.validateSignUpEmail(signUpEmail) - val isPasswordValid = signUpViewModel.validateSignUpPassword(signUpPassword) + val signUpResult by signUpViewModel.signUpResult.observeAsState() + val context = LocalContext.current + + LaunchedEffect(signUpResult) { // 얘는 UI 로직이라고 봐야겠죠? + when (signUpResult) { + is Success -> { + onSignUpComplete() + WavveUtils.showToast( + context = context, + message = R.string.sign_up_success + ) + signUpViewModel.initSignUpResult() + } + + is FailureDuplicateUsername -> { + WavveUtils.showToast( + context = context, + message = R.string.sign_up_failed_duplicate_username + ) + signUpViewModel.initSignUpResult() + } - if (isEmailValid && isPasswordValid) { - onSignUpComplete(signUpEmail, signUpPassword) - Toast.makeText( - context, - context.getString(R.string.sign_up_success), - Toast.LENGTH_SHORT - ).show() - } else if (!isEmailValid) { - Toast.makeText( - context, - context.getString(R.string.sign_up_failed_email), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.sign_up_failed_password), - Toast.LENGTH_SHORT - ).show() + is FailureInformationLength -> { + WavveUtils.showToast( + context = context, + message = R.string.sign_up_failed_information_length + ) + signUpViewModel.initSignUpResult() } + + else -> {} + } + } + + Button( + onClick = { + signUpViewModel.signUp( + signUpUsername = signUpUsername, + signUpPassword = signUpPassword, + signUpHobby = signUpHobby + ) }, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/sopt/and/signup/components/SignUpEmailField.kt b/app/src/main/java/org/sopt/and/signup/components/SignUpEmailField.kt index 525b4ca..89d40e4 100644 --- a/app/src/main/java/org/sopt/and/signup/components/SignUpEmailField.kt +++ b/app/src/main/java/org/sopt/and/signup/components/SignUpEmailField.kt @@ -11,22 +11,22 @@ import org.sopt.and.components.CautionBox import org.sopt.and.components.SignInOrSignUpTextField @Composable -fun SignUpEmailField( - email: String, - onSignUpEmailChange: (String) -> Unit +fun SignUpUsernameField( + signUpUsername: String, + onSignUpUsernameChange: (String) -> Unit ) { Column { SignInOrSignUpTextField( - emailOrPassword = email, - onValueChange = onSignUpEmailChange, - placeholder = R.string.sign_up_email_placeholder + information = signUpUsername, + onValueChange = onSignUpUsernameChange, + placeholder = R.string.sign_up_username_placeholder ) Spacer(modifier = Modifier.height(10.dp)) CautionBox( - contentDescription = R.string.sign_up_email_description, - caution = R.string.sign_up_email_caution + contentDescription = R.string.sign_up_username_description, + caution = R.string.sign_up_username_caution ) } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/components/SignUpGreetingText.kt b/app/src/main/java/org/sopt/and/signup/components/SignUpGreetingText.kt index 07ee495..639d065 100644 --- a/app/src/main/java/org/sopt/and/signup/components/SignUpGreetingText.kt +++ b/app/src/main/java/org/sopt/and/signup/components/SignUpGreetingText.kt @@ -1,72 +1,70 @@ package org.sopt.and.signup.components -import android.content.Context import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.LineHeightStyle -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat.getString import org.sopt.and.R import org.sopt.and.ui.theme.Grey200 import org.sopt.and.ui.theme.White100 @Composable -fun SignUpGreetingText( - fontSize: Int, - context: Context -) { +fun SignUpGreetingText(fontSize: Int) { Text( - buildAnnotatedString { - withStyle( - style = ParagraphStyle( - lineHeight = 2.em, - lineHeightStyle = LineHeightStyle( - alignment = LineHeightStyle.Alignment.Top, - trim = LineHeightStyle.Trim.Both - ) - ) - ) { - withStyle( - style = SpanStyle( + AnnotatedString( + text = stringResource(R.string.sign_up_welcom_text), + spanStyles = listOf( + AnnotatedString.Range( + item = SpanStyle( color = White100, fontSize = fontSize.sp - ) - ) { - append(getString(context, R.string.sign_up_focused_welcome_text_first_line)) - } - - withStyle( - style = SpanStyle( + ), + start = 0, + end = 9 + ), + AnnotatedString.Range( + item = SpanStyle( color = Grey200, fontSize = fontSize.sp - ) - ) { - append(getString(context, R.string.sign_up_remainder_welcome_text_first_line)) - } - - withStyle( - style = SpanStyle( + ), + start = 9, + end = 12 + ), + AnnotatedString.Range( + item = SpanStyle( color = White100, fontSize = fontSize.sp - ) - ) { - append(getString(context, R.string.sign_up_focused_welcome_text_second_line)) - } - - withStyle( - style = SpanStyle( + ), + start = 13, + end = 24 + ), + AnnotatedString.Range( + item = SpanStyle( color = Grey200, fontSize = fontSize.sp - ) - ) { - append(getString(context, R.string.sign_up_remainder_welcome_text_second_line)) - } - } - } + ), + start = 24, + end = 29 + ), + ), + paragraphStyles = listOf( + AnnotatedString.Range( + item = ParagraphStyle( + lineHeight = 2.em, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Top, + trim = LineHeightStyle.Trim.Both + ) + ), + start = 0, + end = 29 + ) + ) + ) ) } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/components/SignUpHobbyField.kt b/app/src/main/java/org/sopt/and/signup/components/SignUpHobbyField.kt new file mode 100644 index 0000000..0c2ff45 --- /dev/null +++ b/app/src/main/java/org/sopt/and/signup/components/SignUpHobbyField.kt @@ -0,0 +1,17 @@ +package org.sopt.and.signup.components + +import androidx.compose.runtime.Composable +import org.sopt.and.R +import org.sopt.and.components.SignInOrSignUpTextField + +@Composable +fun SignUpHobbyField( + signUpHobby: String, + onSignUpHobbyChange: (String) -> Unit +) { + SignInOrSignUpTextField( + information = signUpHobby, + onValueChange = onSignUpHobbyChange, + placeholder = R.string.sign_up_hobby_placeholder + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/signup/components/SignUpPasswordField.kt b/app/src/main/java/org/sopt/and/signup/components/SignUpPasswordField.kt index c844196..eb393a8 100644 --- a/app/src/main/java/org/sopt/and/signup/components/SignUpPasswordField.kt +++ b/app/src/main/java/org/sopt/and/signup/components/SignUpPasswordField.kt @@ -21,7 +21,7 @@ fun SignUpPasswordField( ) { Column { SignInOrSignUpTextField( - emailOrPassword = signUpPassword, + information = signUpPassword, onValueChange = onSignUpPasswordChange, placeholder = R.string.sign_up_password_placeholder, visualTransformation = transformationPasswordVisual(isSignUpPasswordVisible), diff --git a/app/src/main/java/org/sopt/and/signup/dto/SignUpRequestDto.kt b/app/src/main/java/org/sopt/and/signup/dto/SignUpRequestDto.kt new file mode 100644 index 0000000..b7036b8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/signup/dto/SignUpRequestDto.kt @@ -0,0 +1,10 @@ +package org.sopt.and.signup.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SignUpRequestDto( + val username: String, + val password: String, + val hobby: String +) diff --git a/app/src/main/java/org/sopt/and/signup/dto/SignUpResponseDto.kt b/app/src/main/java/org/sopt/and/signup/dto/SignUpResponseDto.kt new file mode 100644 index 0000000..3ecff55 --- /dev/null +++ b/app/src/main/java/org/sopt/and/signup/dto/SignUpResponseDto.kt @@ -0,0 +1,14 @@ +package org.sopt.and.signup.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class SignUpResponseDto( + val result: SignUpResponseResultDto? = null, + val code: String? = null +) + +@Serializable +data class SignUpResponseResultDto( + val no: Int +) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be99da0..1fdd180 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,21 +15,19 @@ 회원가입 - wavve@example.com + wavve Username 설정 wavve 비밀번호 설정 + 취미입력 ex)soccer Wavve 회원가입 회원가입에 성공했습니다~ - 이메일 양식에 맞게 입력해주세요! - 비밀번호 양식에 맞게 입력해주세요! - 비밀번호는 8~20자 이내로 영문 대소문자, 숫자, 특수문자 중\n3가지 이상 혼용하여 입력해 주세요. - 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해\n주세요 - 이메일 입력시 주의사항 + Username이 중복되었습니다! + username, 비밀번호, 취미는 8자 이하로 입력해주세요! + 비밀번호는 8자 이하로 입력해 주세요 + Username는 8자 이하로 입력해 주세요 + Username 입력시 주의사항 비밀번호 입력시 주의사항 회원가입 화면 닫기 - 이메일과 비밀번호 - 만으로\n - Wavve를 즐길 수 - 있어요! + 이메일과 비밀번호만으로\nWavve를 즐길 수 있어요! 또는 다른 서비스 계정으로 가입 @@ -37,10 +35,11 @@ 아이디 찾기 비밀번호 재설정 회원가입 - 이메일 주소 또는 아이디 + Username 비밀번호 로그인 성공. 환영합니다~ - 로그인 실패. 다시 시도해 주세요 + 로그인 실패. 비밀번호는 8자리 이하입니다! + 로그인 실패. 비밀번호가 일치하지 않습니다! 이전 화면으로 가기 또는 다른 서비스 계정으로 로그인 @@ -60,8 +59,8 @@ 믿고 보는 웨이브 에디터 추천작 오늘의 TOP 20 - %1$s - " | %1$s" + %1$s  + | %1$s diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd8976e..3518301 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] -agp = "8.7.0" +agp = "8.7.2" +datastorePreferences = "1.0.0" kotlin = "2.0.0" coreKtx = "1.10.1" junit = "4.13.2" @@ -11,9 +12,14 @@ composeBom = "2024.04.01" androidxComposeNavigation = "2.8.3" kotlinxSerializationJson = "1.7.3" lifecycle-viewmodel-compose = "2.8.6" +runtimeLivedata = "1.7.5" +okhttp = "4.11.0" +retrofit = "2.11.0" +retrofitKotlinSerializationConverter = "1.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -30,6 +36,13 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" } +androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } + +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" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }