diff --git a/app/build.gradle b/app/build.gradle index 28b5267..f0ea816 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,12 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' } +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) + android { namespace 'com.sopt.now.compose' compileSdk 34 @@ -18,6 +22,8 @@ android { vectorDrawables { useSupportLibrary true } + buildConfigField "String", "AUTH_BASE_URL", properties["base.url"] + buildConfigField "String", "FOLLOWER_URL", properties["follower.url"] } buildTypes { @@ -35,9 +41,10 @@ android { } buildFeatures { compose true + buildConfig true } composeOptions { - kotlinCompilerExtensionVersion '1.5.1' + kotlinCompilerExtensionVersion '1.5.5' } packagingOptions { resources { @@ -47,10 +54,10 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2024.04.00') + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.0' + implementation 'androidx.activity:activity-compose:1.9.0' + implementation platform('androidx.compose:compose-bom:2024.05.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' @@ -59,9 +66,29 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.04.00') + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' - implementation 'androidx.compose.material:material:1.6.6' + implementation 'androidx.compose.material:material:1.6.7' + + // Lifecycle Viewmodel + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0' + + // Fragment && Activity + implementation 'androidx.fragment:fragment-ktx:1.7.1' + implementation 'androidx.activity:activity-ktx:1.9.0' + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1' + implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0' + + // OKHttp + implementation platform('com.squareup.okhttp3:okhttp-bom:4.10.0') + implementation 'com.squareup.okhttp3:okhttp' + implementation 'com.squareup.okhttp3:logging-interceptor' + + // Coil + implementation 'io.coil-kt:coil-compose:2.6.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a9fa545..f73f094 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,11 @@ + + , + val support: SupportDto, +) + +@Serializable +data class UserDataDto( + val id: Int, + val email: String, + @SerialName("first_name") val firstName: String, + @SerialName("last_name") val lastName: String, + val avatar: String, +) + +@Serializable +data class SupportDto( + val url: String, + val text: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/model/SignInState.kt b/app/src/main/java/com/sopt/now/compose/data/model/SignInState.kt new file mode 100644 index 0000000..6b13d90 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/model/SignInState.kt @@ -0,0 +1,6 @@ +package com.sopt.now.compose.data.model + +data class SignInState( + val isSuccess: Boolean, + val message: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/model/SignUpState.kt b/app/src/main/java/com/sopt/now/compose/data/model/SignUpState.kt new file mode 100644 index 0000000..89c9675 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/model/SignUpState.kt @@ -0,0 +1,6 @@ +package com.sopt.now.compose.data.model + +data class SignUpState( + val isSuccess: Boolean, + val message: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/module/ApiFactory.kt b/app/src/main/java/com/sopt/now/compose/data/module/ApiFactory.kt new file mode 100644 index 0000000..b874bde --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/module/ApiFactory.kt @@ -0,0 +1,43 @@ +package com.sopt.now.compose.data.module + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.sopt.now.compose.BuildConfig +import com.sopt.now.compose.data.network.AuthService +import com.sopt.now.compose.data.network.FollowerService +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.create + + +object ApiFactory { + private const val BASE_URL: String = BuildConfig.AUTH_BASE_URL + private const val FOLLOWER_URL: String = BuildConfig.FOLLOWER_URL + + private val interceptorClient = OkHttpClient().newBuilder() + .addInterceptor(AuthInterceptor()).build() + + val baseRetrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(interceptorClient) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + val followerRetrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(FOLLOWER_URL) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } + + inline fun createBaseRetrofit(): T = baseRetrofit.create() + inline fun createFollowerRetrofit(): T = followerRetrofit.create() +} + +object ServicePool { + val authService = ApiFactory.createBaseRetrofit() + val followerService = ApiFactory.createFollowerRetrofit() +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/module/AuthInterceptor.kt b/app/src/main/java/com/sopt/now/compose/data/module/AuthInterceptor.kt new file mode 100644 index 0000000..eabf116 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/module/AuthInterceptor.kt @@ -0,0 +1,17 @@ +package com.sopt.now.compose.data.module + +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + + val newRequest = originalRequest.newBuilder() + .addHeader("memberId", "514") + .build() + + return chain.proceed(newRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/network/AuthService.kt b/app/src/main/java/com/sopt/now/compose/data/network/AuthService.kt new file mode 100644 index 0000000..5652628 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/network/AuthService.kt @@ -0,0 +1,27 @@ +package com.sopt.now.compose.data.network + +import com.sopt.now.compose.data.model.RequestSignInDto +import com.sopt.now.compose.data.model.RequestSignUpDto +import com.sopt.now.compose.data.model.ResponseInfoDto +import com.sopt.now.compose.data.model.ResponseSignInDto +import com.sopt.now.compose.data.model.ResponseSignUpDto +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface AuthService { + @POST("member/join") + fun signUp( + @Body request: RequestSignUpDto, + ): Call + + @POST("member/login") + fun signIn( + @Body request: RequestSignInDto, + ): Call + + @GET("member/info") + fun memberInfo( + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/data/network/FollowerService.kt b/app/src/main/java/com/sopt/now/compose/data/network/FollowerService.kt new file mode 100644 index 0000000..e9fbe43 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/data/network/FollowerService.kt @@ -0,0 +1,13 @@ +package com.sopt.now.compose.data.network + +import com.sopt.now.compose.data.model.ResponseUserDto +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +interface FollowerService { + @GET("/api/users") + fun getUserList( + @Query("page") page: Int, + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/MainScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/MainScreen.kt index 77297f9..b460dae 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/MainScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/MainScreen.kt @@ -15,10 +15,6 @@ import com.sopt.now.compose.ui.search.SearchScreen @Composable fun MainScreen( - id: String, - pw: String, - nickname: String, - mbti: String, ) { val navController = rememberNavController() Scaffold( @@ -36,7 +32,7 @@ fun MainScreen( SearchScreen() } composable(Screen.MyPage.route) { - MyPageScreen(id, pw, nickname, mbti) + MyPageScreen() } } } diff --git a/app/src/main/java/com/sopt/now/compose/ui/base/SoptAppNavHost.kt b/app/src/main/java/com/sopt/now/compose/ui/base/SoptAppNavHost.kt index 8cc9bc7..58ebb65 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/base/SoptAppNavHost.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/base/SoptAppNavHost.kt @@ -2,80 +2,32 @@ package com.sopt.now.compose.ui.base import androidx.compose.runtime.Composable import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import com.sopt.now.compose.ui.MainScreen -import com.sopt.now.compose.ui.login.LoginScreen +import com.sopt.now.compose.ui.signIn.SignInScreen import com.sopt.now.compose.ui.signUp.SignUpScreen @Composable fun SoptAppNavHost() { val navController: NavHostController = rememberNavController() - NavHost(navController = navController, startDestination = "login") { - composable( - "login?id={id}&pw={pw}&nickname={nickname}&mbti={mbti}", arguments = listOf( - navArgument("id") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("pw") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("nickname") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("mbti") { - type = NavType.StringType - defaultValue = "" - }, - ) - ) { backStackEntry -> - val id = backStackEntry.arguments?.getString("id") - val pw = backStackEntry.arguments?.getString("pw") - val nickname = backStackEntry.arguments?.getString("nickname") - val mbti = backStackEntry.arguments?.getString("mbti") - LoginScreen( + NavHost(navController = navController, startDestination = "sign_in") { + composable("sign_in") { + SignInScreen( onNavigateToHome = navController, onNavigateToSignUp = { navController.navigate("signup") }, - id.toString(), pw.toString(), nickname.toString(), mbti.toString() ) } composable( - "main?id={id}&pw={pw}&nickname={nickname}&mbti={mbti}", - arguments = listOf( - navArgument("id") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("pw") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("nickname") { - type = NavType.StringType - defaultValue = "" - }, - navArgument("mbti") { - type = NavType.StringType - defaultValue = "" - }, - ), - ) { backStackEntry -> - val id = backStackEntry.arguments?.getString("id") - val pw = backStackEntry.arguments?.getString("pw") - val nickname = backStackEntry.arguments?.getString("nickname") - val mbti = backStackEntry.arguments?.getString("mbti") - MainScreen(id.toString(), pw.toString(), nickname.toString(), mbti.toString()) + "main", + ) { + MainScreen() } composable("signup") { - SignUpScreen(onNavigateToLogin = navController) + SignUpScreen(onNavigateToSignIn = navController) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/home/HomeFriendProfile.kt b/app/src/main/java/com/sopt/now/compose/ui/home/HomeFriendProfile.kt index 17617bf..4760514 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/home/HomeFriendProfile.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/home/HomeFriendProfile.kt @@ -1,6 +1,5 @@ package com.sopt.now.compose.ui.home -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -14,12 +13,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage @Composable fun HomeFriendProfile( - profileImage: Int, + profileImage: String, name: String, description: String, ) { @@ -29,8 +28,8 @@ fun HomeFriendProfile( .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - Image( - painter = painterResource(id = profileImage), + AsyncImage( + model = profileImage, contentDescription = null, modifier = Modifier .width(36.dp) diff --git a/app/src/main/java/com/sopt/now/compose/ui/home/HomeScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/home/HomeScreen.kt index ec67019..ec787af 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/home/HomeScreen.kt @@ -2,85 +2,28 @@ package com.sopt.now.compose.ui.home import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel import com.sopt.now.compose.R -import com.sopt.now.compose.data.Profile @Composable -fun HomeScreen() { - val profileList: List = - listOf( - Profile( - profileImage = R.drawable.img_profile0, - name = "의피티", - description = "김아린 과제 마감 30분전" - ), - Profile( - profileImage = R.drawable.img_profile1, - name = "최준서", - description = "오운완 ㅋㅋ", - ), - Profile( - profileImage = R.drawable.img_profile2, - name = "이연진", - description = "아리니 넘 기엽당..", - ), - Profile( - profileImage = R.drawable.img_profile3, - name = "손민재", - description = "점심 뭐 먹지?", - ), - Profile( - profileImage = R.drawable.img_profile4, - name = "홍해인", - description = "난 눈물의 여왕이야", - ), - Profile( - profileImage = R.drawable.img_profile5, - name = "백현우", - description = "눈물의여왕시작하지말걸공부가안된다", - ), - Profile( - profileImage = R.drawable.img_profile6, - name = "이서경", - description = "저는 환연 과몰입러예요", - ), - Profile( - profileImage = R.drawable.img_profile7, - name = "이주원", - description = "너가 자기야 미안해 했잖아?", - ), - Profile( - profileImage = R.drawable.img_profile8, - name = "김광태", - description = "내일 뭐 해?", - ), - Profile( - profileImage = R.drawable.img_profile9, - name = "정현규", - description = "내봬누", - ), - Profile( - profileImage = R.drawable.img_profile10, - name = "성해은", - description = "벌써 스물 아홉이야", - ), - Profile( - profileImage = R.drawable.img_profile11, - name = "정규민", - description = "오마카세 사줄게", - ), - ) +fun HomeScreen(homeViewModel: HomeViewModel = viewModel()) { + val followerState by homeViewModel.followerState.collectAsState() LazyColumn(modifier = Modifier.fillMaxSize()) { - item { HomeMyProfile(R.drawable.img_arin, "김아린", "업보 청산의 끝이 보인다.") } - items(profileList.size) { index -> + item { + HomeMyProfile(R.drawable.img_arin, "김아린", "업보 청산의 끝이 보인다.") + } + items(followerState) { follower -> HomeFriendProfile( - profileImage = profileList[index].profileImage, - name = profileList[index].name, - description = profileList[index].description, + profileImage = follower.avatar, + name = "${follower.firstName} ${follower.lastName}", + description = follower.email, ) } } diff --git a/app/src/main/java/com/sopt/now/compose/ui/home/HomeViewModel.kt b/app/src/main/java/com/sopt/now/compose/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..a79e0bb --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/home/HomeViewModel.kt @@ -0,0 +1,59 @@ +package com.sopt.now.compose.ui.home + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.sopt.now.compose.data.model.Profile +import com.sopt.now.compose.data.model.ResponseUserDto +import com.sopt.now.compose.data.model.UserDataDto +import com.sopt.now.compose.data.module.ServicePool +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class HomeViewModel : ViewModel() { + private val followerService by lazy { ServicePool.followerService } + + private val _followerState = MutableStateFlow>(emptyList()) + val followerState = _followerState.asStateFlow() + + val friendList = mutableListOf() + + init { + fetchFollowerList() + } + + private fun fetchFollowerList() { + followerService.getUserList(page = 0).enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + val data = response.body()?.data + if (data != null) { + _followerState.value = data + mapFollowersToFriendList(data) + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e("HomeError", "${t.message}") + } + }) + } + + fun mapFollowersToFriendList(followers: List) { + for (follower in followers) { + friendList.add( + Profile( + profileImage = follower.avatar, + name = "${follower.firstName} ${follower.lastName}", + description = follower.email + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/login/LoginScreen.kt deleted file mode 100644 index 14dd04c..0000000 --- a/app/src/main/java/com/sopt/now/compose/ui/login/LoginScreen.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.sopt.now.compose.ui.login - -import android.util.Log -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.NavHostController -import com.sopt.now.compose.R -import com.sopt.now.compose.ui.base.SoptInputTextField -import com.sopt.now.compose.ui.base.SoptOutlinedButton -import com.sopt.now.compose.ui.base.SoptPasswordTextField - -@Composable -fun LoginScreen( - onNavigateToHome: NavHostController, - onNavigateToSignUp: () -> Unit, - id: String, - pw: String, - nickname: String, - mbti: String, -) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween - ) { - var textId by remember { - mutableStateOf("") - } - var textPw by remember { - mutableStateOf("") - } - - - val isLoginButtonEnabled by remember(textId, textPw) { - mutableStateOf( - textId.isNotEmpty() && textPw.isNotEmpty() - ) - } - - Text( - text = stringResource(id = R.string.login_screen_title), - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(16.dp) - ) - Column { - SoptInputTextField( - text = R.string.label_id, value = textId, onValueChange = { textId = it } - ) - SoptPasswordTextField( - text = R.string.label_pw, value = textPw, onValueChange = { textPw = it } - ) - } - - Column { - SoptOutlinedButton( - text = R.string.btn_login, - onClick = { - Log.d("Login", "$textId, $id") - if (textId == id && textPw == pw && isLoginButtonEnabled) { - onNavigateToHome.navigate("main?id=$textId&pw=$textPw&nickname=$nickname&mbti=$mbti") - } - }, - enabled = true - ) - SoptOutlinedButton( - text = R.string.btn_sign_up, - onClick = onNavigateToSignUp, - enabled = true - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageScreen.kt index 8110da6..e68a4b5 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageScreen.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember @@ -16,15 +18,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.sopt.now.compose.R @Composable fun MyPageScreen( - id: String, - pw: String, - nickname: String, - mbti: String, + myPageViewModel: MyPageViewModel = viewModel(), ) { + val infoState by myPageViewModel.infoState.collectAsState() var offset by remember { mutableFloatStateOf(0f) } Column(modifier = Modifier @@ -42,11 +44,10 @@ fun MyPageScreen( contentDescription = null, modifier = Modifier.aspectRatio(3f / 4f) ) - Column { - Text(text = id) - Text(text = pw) - Text(text = nickname) - Text(text = mbti) + Column(modifier = Modifier.padding(16.dp)) { + Text(text = infoState.data.authenticationId) + Text(text = infoState.data.nickname) + Text(text = infoState.data.phone) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageViewModel.kt b/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageViewModel.kt new file mode 100644 index 0000000..4a773bc --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/myPage/MyPageViewModel.kt @@ -0,0 +1,58 @@ +package com.sopt.now.compose.ui.myPage + +import androidx.lifecycle.ViewModel +import com.sopt.now.compose.data.model.ResponseInfoDto +import com.sopt.now.compose.data.model.UserInfoDto +import com.sopt.now.compose.data.module.ServicePool +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MyPageViewModel : ViewModel() { + private val authService by lazy { ServicePool.authService } + + private val _infoState = + MutableStateFlow( + ResponseInfoDto( + code = 0, + message = "", + data = UserInfoDto(authenticationId = "", nickname = "", phone = "") + ) + ) + val infoState = _infoState.asStateFlow() + + init { + fetchInfo() + } + + private fun fetchInfo() { + authService.memberInfo().enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + val data = response.body() + if (data != null) { + _infoState.update { + data + } + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + _infoState.update { + ResponseInfoDto( + code = -1, + message = "${t.message}", + data = UserInfoDto(authenticationId = "", nickname = "", phone = "") + ) + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInScreen.kt new file mode 100644 index 0000000..1fd0b28 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInScreen.kt @@ -0,0 +1,109 @@ +package com.sopt.now.compose.ui.signIn + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import com.sopt.now.compose.R +import com.sopt.now.compose.data.model.RequestSignInDto +import com.sopt.now.compose.ui.base.SoptInputTextField +import com.sopt.now.compose.ui.base.SoptOutlinedButton +import com.sopt.now.compose.ui.base.SoptPasswordTextField + +@Composable +fun SignInScreen( + onNavigateToHome: NavHostController, + onNavigateToSignUp: () -> Unit, + signInViewModel: SignInViewModel = viewModel(), +) { + val context = LocalContext.current + val signInState by signInViewModel.signInState.collectAsState() + + val id by signInViewModel.id.collectAsState() + val password by signInViewModel.password.collectAsState() + + LaunchedEffect(signInState) { + if (signInState.isSuccess) { + Toast.makeText(context, signInState.message, Toast.LENGTH_SHORT).show() + onNavigateToHome.navigate(context.getString(R.string.route_main)) + } else if (signInState.message.isNotBlank()) { + Toast.makeText(context, signInState.message, Toast.LENGTH_SHORT).show() + } + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + val isSignInButtonEnabled by remember(id, password) { + mutableStateOf( + id.isNotEmpty() && password.isNotEmpty() + ) + } + + Text( + text = stringResource(id = R.string.sign_in_screen_title), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp) + ) + Column { + SoptInputTextField( + text = R.string.label_id, + value = id, + onValueChange = { signInViewModel.updateId(it) } + ) + SoptPasswordTextField( + text = R.string.label_pw, + value = password, + onValueChange = { signInViewModel.updatePassword(it) } + ) + } + + Column { + SoptOutlinedButton( + text = R.string.btn_sign_in, + onClick = { + if (isSignInButtonEnabled) { + signInViewModel.signIn( + RequestSignInDto( + authenticationId = id, + password = password, + ) + ) + } else { + Toast.makeText( + context, + R.string.sign_up_fail, + Toast.LENGTH_SHORT + ).show() + } + }, + enabled = true + ) + SoptOutlinedButton( + text = R.string.btn_sign_up, + onClick = onNavigateToSignUp, + enabled = true + ) + } + } +} diff --git a/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInViewModel.kt b/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInViewModel.kt new file mode 100644 index 0000000..42f9c72 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/signIn/SignInViewModel.kt @@ -0,0 +1,69 @@ +package com.sopt.now.compose.ui.signIn + +import androidx.lifecycle.ViewModel +import com.sopt.now.compose.data.model.RequestSignInDto +import com.sopt.now.compose.data.model.ResponseSignInDto +import com.sopt.now.compose.data.model.SignInState +import com.sopt.now.compose.data.module.ServicePool +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class SignInViewModel : ViewModel() { + private val authService by lazy { ServicePool.authService } + + private val _signInState = MutableStateFlow(SignInState(isSuccess = false, message = "")) + val signInState = _signInState.asStateFlow() + + private val _id = MutableStateFlow("") + val id = _id.asStateFlow() + + private val _password = MutableStateFlow("") + val password = _password.asStateFlow() + + fun updateId(newId: String) { + _id.value = newId + } + + fun updatePassword(newPassword: String) { + _password.value = newPassword + } + + + fun signIn(request: RequestSignInDto) { + authService.signIn(request).enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + val userId = response.headers()["location"] + + _signInState.update { + SignInState( + isSuccess = true, + message = "유저 아이디는 $userId" + ) + } + } else { + val error = response.code() + _signInState.update { + SignInState( + isSuccess = false, + message = "로그인 실패 : $error" + ) + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + _signInState.update { SignInState(isSuccess = false, message = "서버 에러") } + } + } + ) + } +} diff --git a/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpScreen.kt b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpScreen.kt index 2f993cc..8fc48dd 100644 --- a/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpScreen.kt @@ -13,11 +13,11 @@ import androidx.compose.foundation.rememberScrollState 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.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -26,18 +26,51 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.sopt.now.compose.R +import com.sopt.now.compose.data.model.RequestSignUpDto import com.sopt.now.compose.ui.base.SoptInputTextField import com.sopt.now.compose.ui.base.SoptOutlinedButton import com.sopt.now.compose.ui.base.SoptPasswordTextField +import com.sopt.now.compose.ui.signUp.SignUpViewModel.Companion.MAX_LENGTH_LOGIN +import com.sopt.now.compose.ui.signUp.SignUpViewModel.Companion.MAX_LENGTH_PASSWORD +import com.sopt.now.compose.ui.signUp.SignUpViewModel.Companion.MIN_LENGTH_LOGIN +import com.sopt.now.compose.ui.signUp.SignUpViewModel.Companion.MIN_LENGTH_PASSWORD import kotlinx.coroutines.launch @Composable fun SignUpScreen( - onNavigateToLogin: NavHostController, + onNavigateToSignIn: NavHostController, + signUpViewModel: SignUpViewModel = viewModel(), ) { val context = LocalContext.current + val signUpEvent by signUpViewModel.signUpEvent.collectAsState() + + val id by signUpViewModel.id.collectAsState() + val password by signUpViewModel.password.collectAsState() + val nickname by signUpViewModel.nickname.collectAsState() + val phoneNumber by signUpViewModel.phoneNumber.collectAsState() + + LaunchedEffect(signUpEvent) { + when (val event = signUpEvent) { + is SignUpSideEffect.Success -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + onNavigateToSignIn.navigate(context.getString(R.string.route_sign_in)) + } + + is SignUpSideEffect.Loading -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + } + + is SignUpSideEffect.Error -> { + Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() + } + + else -> {} + } + } + Column( modifier = Modifier.fillMaxSize(), @@ -54,24 +87,13 @@ fun SignUpScreen( } } - var textId by remember { - mutableStateOf("") - } - var textPw by remember { - mutableStateOf("") - } - var textNickname by remember { - mutableStateOf("") - } - var textMbti by remember { - mutableStateOf("") - } - - val isSignUpButtonEnabled by remember(textId, textPw, textNickname, textMbti) { + val isSignUpButtonEnabled by remember(id, password, nickname, phoneNumber) { mutableStateOf( - textId.length in 6..10 && textPw.length in 8..12 && textNickname.isNotEmpty() && !textNickname.contains( + id.length in MIN_LENGTH_LOGIN..MAX_LENGTH_LOGIN + && password.length in MIN_LENGTH_PASSWORD..MAX_LENGTH_PASSWORD + && nickname.isNotEmpty() && !nickname.contains( " " - ) && textMbti.length == 4 + ) && phoneNumber.matches(Regex("^010-\\d{4}-\\d{4}\$")) ) } @@ -85,37 +107,41 @@ fun SignUpScreen( item { SoptInputTextField( text = R.string.hint_id, - value = textId, - onValueChange = { textId = it }) + value = id, + onValueChange = { signUpViewModel.updateId(it) }) SoptPasswordTextField( text = R.string.hint_pw, - value = textPw, - onValueChange = { textPw = it }) + value = password, + onValueChange = { signUpViewModel.updatePassword(it) }) SoptInputTextField( text = R.string.hint_nickname, - value = textNickname, - onValueChange = { textNickname = it }) + value = nickname, + onValueChange = { signUpViewModel.updateNickname(it) }) SoptInputTextField( - text = R.string.hint_mbti, - value = textMbti, - onValueChange = { textMbti = it }) + text = R.string.hint_phone, + value = phoneNumber, + onValueChange = { signUpViewModel.updatePhoneNumber(it) }) } } SoptOutlinedButton(text = R.string.btn_sign_up, onClick = { if (isSignUpButtonEnabled) { - Toast.makeText( - context, - "회원가입 성공", - Toast.LENGTH_SHORT - ).show() - onNavigateToLogin.navigate("login?id=$textId&pw=$textPw&nickname=$textNickname&mbti=$textMbti") + signUpViewModel.signUp( + RequestSignUpDto( + authenticationId = id, + password = password, + nickname = nickname, + phone = phoneNumber + ) + ) } else { Toast.makeText( context, - "조건을 만족하지 않습니다", + R.string.sign_up_fail_message, Toast.LENGTH_SHORT ).show() } }, enabled = true) } } + + diff --git a/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpSideEffect.kt b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpSideEffect.kt new file mode 100644 index 0000000..cd1cba6 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpSideEffect.kt @@ -0,0 +1,7 @@ +package com.sopt.now.compose.ui.signUp + +sealed class SignUpSideEffect { + data class Success(val message: String) : SignUpSideEffect() + data class Loading(val message: String) : SignUpSideEffect() + data class Error(val message: String) : SignUpSideEffect() +} \ No newline at end of file diff --git a/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpViewModel.kt b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpViewModel.kt new file mode 100644 index 0000000..f8c0020 --- /dev/null +++ b/app/src/main/java/com/sopt/now/compose/ui/signUp/SignUpViewModel.kt @@ -0,0 +1,78 @@ +package com.sopt.now.compose.ui.signUp + +import androidx.lifecycle.ViewModel +import com.sopt.now.compose.data.model.RequestSignUpDto +import com.sopt.now.compose.data.model.ResponseSignUpDto +import com.sopt.now.compose.data.module.ServicePool +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class SignUpViewModel : ViewModel() { + private val authService by lazy { ServicePool.authService } + + private val _signUpEvent = MutableStateFlow(null) + val signUpEvent = _signUpEvent.asStateFlow() + + private val _id = MutableStateFlow("") + val id = _id.asStateFlow() + + private val _password = MutableStateFlow("") + val password = _password.asStateFlow() + + private val _nickname = MutableStateFlow("") + val nickname = _nickname.asStateFlow() + + private val _phoneNumber = MutableStateFlow("") + val phoneNumber = _phoneNumber.asStateFlow() + + fun updateId(id: String) { + _id.value = id + } + + fun updatePassword(password: String) { + _password.value = password + } + + fun updateNickname(nickname: String) { + _nickname.value = nickname + } + + fun updatePhoneNumber(phoneNumber: String) { + _phoneNumber.value = phoneNumber + } + + + fun signUp(request: RequestSignUpDto) { + authService.signUp(request).enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + if (response.isSuccessful) { + val userId = response.headers()["location"] + _signUpEvent.value = SignUpSideEffect.Success("가입된 유저 아이디는 $userId") + } else { + val error = response.code() + _signUpEvent.value = SignUpSideEffect.Error("회원가입 실패 : $error") + } + } + + override fun onFailure(call: Call, t: Throwable) { + _signUpEvent.value = SignUpSideEffect.Error("서버 에러: ${t.message}") + } + }, + ) + } + + companion object { + const val MIN_LENGTH_LOGIN = 6 + const val MAX_LENGTH_LOGIN = 10 + const val MIN_LENGTH_PASSWORD = 8 + const val MAX_LENGTH_PASSWORD = 12 + } +} \ 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 8f65a49..88746f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,25 +1,32 @@ NOW SOPT Android - - Welcome To SOPT! + + Welcome To SOPT! ID Password Nickname - MBTI - 로그인 + 전화번호 + 로그인 회원가입 + 로그인 실패 + 조건을 만족하지 않습니다 Sign Up 아이디를 입력해주세요. 비밀번호를 입력해주세요. 닉네임을 입력해주세요. - MBTI를 입력해주세요. + 전화번호를 입력해주세요. 검색 마이페이지 + + main + sign_in + sign_up + \ No newline at end of file diff --git a/build.gradle b/build.gradle index be6601c..4c76592 100644 --- a/build.gradle +++ b/build.gradle @@ -3,4 +3,5 @@ plugins { id 'com.android.application' version '8.3.1' apply false id 'com.android.library' version '8.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20" apply false } \ No newline at end of file