From 45f5c419b6529bc3eb194b50edaacc8f5a740bfb Mon Sep 17 00:00:00 2001 From: Thirfir Date: Sat, 2 Nov 2024 17:14:14 +0900 Subject: [PATCH 01/41] add: Retrofit dependency --- app/build.gradle.kts | 18 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 1 + data/build.gradle.kts | 8 ++++++++ gradle/libs.versions.toml | 11 ++++++++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f3162cd..6f6f42e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,16 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.kapt) alias(libs.plugins.hilt) + kotlin("plugin.serialization") version "2.0.20" +} + +val properties = Properties().apply { + load(project.rootProject.file("local.properties").inputStream()) } android { @@ -18,6 +25,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BASE_URL", properties["base_url"].toString()) } buildTypes { @@ -36,6 +44,9 @@ android { kotlinOptions { jvmTarget = "11" } + buildFeatures { + buildConfig = true + } } kapt { @@ -66,4 +77,11 @@ dependencies { implementation(libs.hilt) kapt(libs.hilt.compiler) + + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlin.serialization.converter) + implementation(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af279dc..50eca34 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ANDANDROID" + android:usesCleartextTraffic="true" tools:targetApi="31"> Date: Tue, 5 Nov 2024 18:24:59 +0900 Subject: [PATCH 02/41] =?UTF-8?q?add:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/di/ApiModule.kt | 20 ++++++++++ .../java/org/sopt/and/di/NetworkModule.kt | 26 ++++++++++++ .../java/com/sopt/data/api/remote/UserApi.kt | 11 +++++ .../datasource/local/UserLocalDataSource.kt | 7 ---- .../datasource/remote/UserRemoteDataSource.kt | 14 +++++++ .../data/repository/UserRepositoryImpl.kt | 16 ++++++-- .../com/sopt/data/request/SignUpRequest.kt | 11 +++++ .../sopt/domain/repository/UserRepository.kt | 6 ++- .../domain/usecase/SignUpAccountUseCase.kt | 4 +- .../signup/viewmodel/SignUpViewModel.kt | 40 +++++++++++-------- 10 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/sopt/and/di/ApiModule.kt create mode 100644 app/src/main/java/org/sopt/and/di/NetworkModule.kt create mode 100644 data/src/main/java/com/sopt/data/api/remote/UserApi.kt create mode 100644 data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt create mode 100644 data/src/main/java/com/sopt/data/request/SignUpRequest.kt diff --git a/app/src/main/java/org/sopt/and/di/ApiModule.kt b/app/src/main/java/org/sopt/and/di/ApiModule.kt new file mode 100644 index 0000000..9df6e83 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/ApiModule.kt @@ -0,0 +1,20 @@ +package org.sopt.and.di + +import com.sopt.data.api.remote.UserApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ApiModule { + + @Singleton + @Provides + fun provideUserApi(retrofit: Retrofit): UserApi { + return retrofit.create(UserApi::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt new file mode 100644 index 0000000..4d13ceb --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -0,0 +1,26 @@ +package org.sopt.and.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import org.sopt.and.BuildConfig +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Singleton + @Provides + fun provideRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + } +} \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt new file mode 100644 index 0000000..c3ea3df --- /dev/null +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -0,0 +1,11 @@ +package com.sopt.data.api.remote + +import com.sopt.data.request.SignUpRequest +import retrofit2.http.Body +import retrofit2.http.POST + +interface UserApi { + + @POST("user") + suspend fun signUp(@Body signUpRequest: SignUpRequest) +} \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt index 6aad93a..641765e 100644 --- a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt @@ -9,13 +9,6 @@ class UserLocalDataSource @Inject constructor( @UserSharedPref private val userSharedPreferences: SharedPreferences ) { - fun saveAccount(email: String, password: String) { - userSharedPreferences.edit() - .putString(KEY_EMAIL, email) - .putString(KEY_PASSWORD, password) - .apply() - } - fun trySignIn(email: String, password: String): Result { val savedEmail = userSharedPreferences.getString(KEY_EMAIL, null) val savedPassword = userSharedPreferences.getString(KEY_PASSWORD, null) diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt new file mode 100644 index 0000000..a8dbf2e --- /dev/null +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -0,0 +1,14 @@ +package com.sopt.data.datasource.remote + +import com.sopt.data.api.remote.UserApi +import com.sopt.data.request.SignUpRequest +import javax.inject.Inject + +class UserRemoteDataSource @Inject constructor( + private val userApi: UserApi +) { + + suspend fun signUp(signUpRequest: SignUpRequest) { + return userApi.signUp(signUpRequest) + } +} \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 20bc130..1e2607c 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -1,18 +1,28 @@ package com.sopt.data.repository import com.sopt.data.datasource.local.UserLocalDataSource +import com.sopt.data.datasource.remote.UserRemoteDataSource +import com.sopt.data.request.SignUpRequest +import com.sopt.domain.model.User import com.sopt.domain.repository.UserRepository import javax.inject.Inject class UserRepositoryImpl @Inject constructor( - private val userLocalDataSource: UserLocalDataSource + private val userLocalDataSource: UserLocalDataSource, + private val userRemoteDataSource: UserRemoteDataSource ) : UserRepository { - override fun saveAccount(email: String, password: String) { - userLocalDataSource.saveAccount(email, password) + override suspend fun signUp(email: String, password: String, hobby: String): Result { + return runCatching { + userRemoteDataSource.signUp(SignUpRequest(email, password, hobby)) + } } override fun trySignIn(email: String, password: String): Result { return userLocalDataSource.trySignIn(email, password) } + + override suspend fun fetchUser(): Result { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/request/SignUpRequest.kt b/data/src/main/java/com/sopt/data/request/SignUpRequest.kt new file mode 100644 index 0000000..e16c0b4 --- /dev/null +++ b/data/src/main/java/com/sopt/data/request/SignUpRequest.kt @@ -0,0 +1,11 @@ +package com.sopt.data.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SignUpRequest( + @SerialName("email") val email: String, + @SerialName("password") val password: String, + @SerialName("hobby") val hobby: String +) diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index 0145556..8900ad5 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -1,6 +1,10 @@ package com.sopt.domain.repository +import com.sopt.domain.model.User + interface UserRepository { - fun saveAccount(email: String, password: String) + suspend fun signUp(email: String, password: String, hobby: String): Result fun trySignIn(email: String, password: String): Result + + suspend fun fetchUser(): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt index 62545c2..5eca8c2 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt @@ -10,14 +10,14 @@ class SignUpAccountUseCase @Inject constructor( private val userRepository: UserRepository ) { - operator fun invoke(email: String, password: String): Result { + suspend operator fun invoke(email: String, password: String, hobby: String): Result { return runCatching { when { email.isBlank() -> Result.failure(SignUpError.EmailInputEmpty()) password.isBlank() -> Result.failure(SignUpError.PasswordInputEmpty()) email.isValidEmail().not() -> Result.failure(SignUpError.InvalidEmail()) password.isValidPassword().not() -> Result.failure(SignUpError.InvalidPassword()) - else -> Result.success(userRepository.saveAccount(email, password)) + else -> userRepository.signUp(email, password, hobby) } } } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt index 416f7a0..bc29fb3 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt @@ -7,9 +7,9 @@ import com.sopt.domain.usecase.SignUpAccountUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -21,13 +21,11 @@ class SignUpViewModel @Inject constructor( field = MutableStateFlow("") val passwordInput: StateFlow field = MutableStateFlow("") + val hobbyInput: StateFlow + field = MutableStateFlow("") - private val _signUpUiState = - MutableSharedFlow(extraBufferCapacity = 1) - val signUpUiState = _signUpUiState.shareIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000) - ) + val signUpUiState: SharedFlow + field = MutableSharedFlow() fun onEmailInputChanged(email: String) { emailInput.value = email @@ -37,15 +35,25 @@ class SignUpViewModel @Inject constructor( passwordInput.value = password } + fun onHobbyInputChanged(hobby: String) { + hobbyInput.value = hobby + } + fun signUp() { - signUpAccountUseCase(emailInput.value, passwordInput.value).onSuccess { - _signUpUiState.tryEmit(SignUpUiState.Success) - }.onFailure { - when (it) { - is SignUpError.EmailInputEmpty -> _signUpUiState.tryEmit(SignUpUiState.EmailInputEmpty) - is SignUpError.PasswordInputEmpty -> _signUpUiState.tryEmit(SignUpUiState.PasswordInputEmpty) - is SignUpError.InvalidEmail -> _signUpUiState.tryEmit(SignUpUiState.InvalidEmail) - is SignUpError.InvalidPassword -> _signUpUiState.tryEmit(SignUpUiState.InvalidPassword) + viewModelScope.launch { + signUpAccountUseCase( + emailInput.value, + passwordInput.value, + hobbyInput.value + ).onSuccess { + signUpUiState.emit(SignUpUiState.Success) + }.onFailure { + when (it) { + is SignUpError.EmailInputEmpty -> signUpUiState.emit(SignUpUiState.EmailInputEmpty) + is SignUpError.PasswordInputEmpty -> signUpUiState.emit(SignUpUiState.PasswordInputEmpty) + is SignUpError.InvalidEmail -> signUpUiState.emit(SignUpUiState.InvalidEmail) + is SignUpError.InvalidPassword -> signUpUiState.emit(SignUpUiState.InvalidPassword) + } } } } From 9c4c8e611fbd9b562f9334a9fec6333d2de8f5f3 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 5 Nov 2024 18:42:33 +0900 Subject: [PATCH 03/41] =?UTF-8?q?add:=20hobby=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{RegexExtensions.kt => InputValidation.kt} | 1 + .../signup/composable/SignUpContentScreen.kt | 15 ++++++++++++--- .../composable/SignUpInputContentView.kt | 19 ++++++++++++++++++- .../screen/signup/composable/SignUpScreen.kt | 12 ++++++++---- presentation/src/main/res/values/strings.xml | 2 ++ 5 files changed, 41 insertions(+), 8 deletions(-) rename domain/src/main/java/com/sopt/domain/util/{RegexExtensions.kt => InputValidation.kt} (87%) diff --git a/domain/src/main/java/com/sopt/domain/util/RegexExtensions.kt b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt similarity index 87% rename from domain/src/main/java/com/sopt/domain/util/RegexExtensions.kt rename to domain/src/main/java/com/sopt/domain/util/InputValidation.kt index 1caff27..17ba5ef 100644 --- a/domain/src/main/java/com/sopt/domain/util/RegexExtensions.kt +++ b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt @@ -10,3 +10,4 @@ private val passwordRegex by lazy { fun String.isValidEmail(): Boolean = emailRegex.matches(this) fun String.isValidPassword(): Boolean = passwordRegex.matches(this) +fun String.isValidHobby(): Boolean = this.isNotBlank() diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt index 9efba92..b22a316 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.sopt.domain.util.isValidEmail +import com.sopt.domain.util.isValidHobby import com.sopt.domain.util.isValidPassword import com.sopt.presentation.R import com.sopt.presentation.ui.component.button.FullWidthTextButton @@ -31,8 +32,10 @@ fun SignUpContentScreen( modifier: Modifier = Modifier, emailInput: String = "", passwordInput: String = "", + hobbyInput: String = "", onEmailInputChanged: (String) -> Unit = {}, onPasswordInputChanged: (String) -> Unit = {}, + onHobbyInputChanged: (String) -> Unit = {}, onSignUpButtonClicked: () -> Unit = { } ) { @@ -42,10 +45,14 @@ fun SignUpContentScreen( val signUpButtonActivated by remember( emailInput, passwordInput - ) { derivedStateOf { emailInput.isValidEmail() && passwordInput.isValidPassword() } } + ) { + derivedStateOf { + emailInput.isValidEmail() && passwordInput.isValidPassword() && hobbyInput.isValidHobby() + } + } var signUpFailureMessage by remember { mutableStateOf("") } - var toast = Toast.makeText(context, signUpFailureMessage, Toast.LENGTH_SHORT) + val toast = Toast.makeText(context, signUpFailureMessage, Toast.LENGTH_SHORT) Column( modifier = Modifier @@ -64,8 +71,10 @@ fun SignUpContentScreen( .padding(horizontal = 6.dp), emailInput = emailInput, passwordInput = passwordInput, + hobbyInput = hobbyInput, onEmailInputChanged = onEmailInputChanged, - onPasswordInputChanged = onPasswordInputChanged + onPasswordInputChanged = onPasswordInputChanged, + onHobbyInputChanged = onHobbyInputChanged ) } FullWidthTextButton( diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt index e0a6a26..252ca04 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt @@ -40,8 +40,10 @@ fun SignUpInputContentView( modifier: Modifier = Modifier, emailInput: String = "", passwordInput: String = "", + hobbyInput: String = "", onEmailInputChanged: (String) -> Unit = {}, - onPasswordInputChanged: (String) -> Unit = {} + onPasswordInputChanged: (String) -> Unit = {}, + onHobbyInputChanged: (String) -> Unit = {} ) { var isPasswordVisible by remember { mutableStateOf(false) } @@ -126,6 +128,21 @@ fun SignUpInputContentView( painter = painterResource(R.drawable.ic_caution), ) + FilledTextField( + modifier = Modifier + .padding(top = 20.dp) + .fillMaxWidth(), + value = hobbyInput, + onValueChange = onHobbyInputChanged, + placeholder = stringResource(R.string.hobby_input_placeholder) + ) + + IconFrontText( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(R.string.sign_up_caution_hobby, 8), + painter = painterResource(R.drawable.ic_caution), + ) + CenterContentHorizontalDivider( modifier = Modifier.padding(top = 56.dp) ) { diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt index 94ad48f..ec58b88 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt @@ -10,6 +10,7 @@ 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.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -39,8 +40,9 @@ fun SignUpScreen( viewModel: SignUpViewModel = hiltViewModel(), ) { - val emailInput = viewModel.emailInput.collectAsStateWithLifecycle() - val passwordInput = viewModel.passwordInput.collectAsStateWithLifecycle() + val emailInput by viewModel.emailInput.collectAsStateWithLifecycle() + val passwordInput by viewModel.passwordInput.collectAsStateWithLifecycle() + val hobbyInput by viewModel.hobbyInput.collectAsStateWithLifecycle() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -82,10 +84,12 @@ fun SignUpScreen( ) { SignUpContentScreen( modifier = Modifier.padding(horizontal = 12.dp), - emailInput = emailInput.value, - passwordInput = passwordInput.value, + emailInput = emailInput, + passwordInput = passwordInput, + hobbyInput = hobbyInput, onEmailInputChanged = viewModel::onEmailInputChanged, onPasswordInputChanged = viewModel::onPasswordInputChanged, + onHobbyInputChanged = viewModel::onHobbyInputChanged, onSignUpButtonClicked = viewModel::signUp ) } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 8b43c76..954de7c 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -40,7 +40,9 @@ wavve@example.com 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요. Wavve 비밀번호 설정 + 취미 설정 비밀번호는 %s~%s자 이내로 영문 대소문자, 숫자, 특수문자 중 3가지 이상 혼용하여 입력해 주세요. + 취미는 %s자 이내로 입력해주세요. 또는 다른 서비스 계정으로 가입 SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다. 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요. Wavve 회원가입 From 6d30f25796cce3bec841ad54b331b1dd91f21da0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 5 Nov 2024 18:47:50 +0900 Subject: [PATCH 04/41] fix: email -> username --- app/build.gradle.kts | 4 ++-- .../sopt/data/repository/UserRepositoryImpl.kt | 4 ++-- .../com/sopt/data/request/SignUpRequest.kt | 2 +- .../com/sopt/domain/exception/ErrorType.kt | 4 ++-- .../sopt/domain/repository/UserRepository.kt | 2 +- .../domain/usecase/SignUpAccountUseCase.kt | 10 +++++----- .../com/sopt/domain/util/InputValidation.kt | 2 +- .../signup/composable/SignUpContentScreen.kt | 18 +++++++++--------- .../composable/SignUpInputContentView.kt | 10 +++++----- .../screen/signup/composable/SignUpScreen.kt | 10 +++++----- .../screen/signup/viewmodel/SignUpViewModel.kt | 16 ++++++++-------- presentation/src/main/res/values/strings.xml | 2 +- 12 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f6f42e..0c689a9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ plugins { kotlin("plugin.serialization") version "2.0.20" } -val properties = Properties().apply { +val localProperties = Properties().apply { load(project.rootProject.file("local.properties").inputStream()) } @@ -25,7 +25,7 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - buildConfigField("String", "BASE_URL", properties["base_url"].toString()) + buildConfigField("String", "BASE_URL", "String.valueOf(\"${localProperties["base_url"]}\")") } buildTypes { diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 1e2607c..ce6470c 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -12,9 +12,9 @@ class UserRepositoryImpl @Inject constructor( private val userRemoteDataSource: UserRemoteDataSource ) : UserRepository { - override suspend fun signUp(email: String, password: String, hobby: String): Result { + override suspend fun signUp(username: String, password: String, hobby: String): Result { return runCatching { - userRemoteDataSource.signUp(SignUpRequest(email, password, hobby)) + userRemoteDataSource.signUp(SignUpRequest(username, password, hobby)) } } diff --git a/data/src/main/java/com/sopt/data/request/SignUpRequest.kt b/data/src/main/java/com/sopt/data/request/SignUpRequest.kt index e16c0b4..ead75b9 100644 --- a/data/src/main/java/com/sopt/data/request/SignUpRequest.kt +++ b/data/src/main/java/com/sopt/data/request/SignUpRequest.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class SignUpRequest( - @SerialName("email") val email: String, + @SerialName("username") val username: String, @SerialName("password") val password: String, @SerialName("hobby") val hobby: String ) diff --git a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt b/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt index 56d6cd8..f2f8801 100644 --- a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt +++ b/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt @@ -8,8 +8,8 @@ sealed class SignInError : Throwable() { } sealed class SignUpError : Throwable() { - class InvalidEmail() : SignUpError() + class InvalidUsername() : SignUpError() class InvalidPassword() : SignUpError() - class EmailInputEmpty() : SignUpError() + class UsernameInputEmpty() : SignUpError() class PasswordInputEmpty() : SignUpError() } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index 8900ad5..e4b57f4 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -3,7 +3,7 @@ package com.sopt.domain.repository import com.sopt.domain.model.User interface UserRepository { - suspend fun signUp(email: String, password: String, hobby: String): Result + suspend fun signUp(username: String, password: String, hobby: String): Result fun trySignIn(email: String, password: String): Result suspend fun fetchUser(): Result diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt index 5eca8c2..f41f1a6 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt @@ -2,22 +2,22 @@ package com.sopt.domain.usecase import com.sopt.domain.exception.SignUpError import com.sopt.domain.repository.UserRepository -import com.sopt.domain.util.isValidEmail import com.sopt.domain.util.isValidPassword +import com.sopt.domain.util.isValidUsername import javax.inject.Inject class SignUpAccountUseCase @Inject constructor( private val userRepository: UserRepository ) { - suspend operator fun invoke(email: String, password: String, hobby: String): Result { + suspend operator fun invoke(username: String, password: String, hobby: String): Result { return runCatching { when { - email.isBlank() -> Result.failure(SignUpError.EmailInputEmpty()) + username.isBlank() -> Result.failure(SignUpError.UsernameInputEmpty()) password.isBlank() -> Result.failure(SignUpError.PasswordInputEmpty()) - email.isValidEmail().not() -> Result.failure(SignUpError.InvalidEmail()) + username.isValidUsername().not() -> Result.failure(SignUpError.InvalidUsername()) password.isValidPassword().not() -> Result.failure(SignUpError.InvalidPassword()) - else -> userRepository.signUp(email, password, hobby) + else -> userRepository.signUp(username, password, hobby) } } } diff --git a/domain/src/main/java/com/sopt/domain/util/InputValidation.kt b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt index 17ba5ef..e8167c3 100644 --- a/domain/src/main/java/com/sopt/domain/util/InputValidation.kt +++ b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt @@ -8,6 +8,6 @@ private val passwordRegex by lazy { Regex("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&#.~_-])[A-Za-z\\d@$!%*?&#.~_-]{8,20}$") } -fun String.isValidEmail(): Boolean = emailRegex.matches(this) +fun String.isValidUsername(): Boolean = this.length in 1..8 fun String.isValidPassword(): Boolean = passwordRegex.matches(this) fun String.isValidHobby(): Boolean = this.isNotBlank() diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt index b22a316..fd44696 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import com.sopt.domain.util.isValidEmail +import com.sopt.domain.util.isValidUsername import com.sopt.domain.util.isValidHobby import com.sopt.domain.util.isValidPassword import com.sopt.presentation.R @@ -30,10 +30,10 @@ import com.sopt.presentation.ui.component.button.FullWidthTextButton @Composable fun SignUpContentScreen( modifier: Modifier = Modifier, - emailInput: String = "", + usernameInput: String = "", passwordInput: String = "", hobbyInput: String = "", - onEmailInputChanged: (String) -> Unit = {}, + onUsernameInputChanged: (String) -> Unit = {}, onPasswordInputChanged: (String) -> Unit = {}, onHobbyInputChanged: (String) -> Unit = {}, onSignUpButtonClicked: () -> Unit = { } @@ -43,11 +43,11 @@ fun SignUpContentScreen( val keyboardController = LocalSoftwareKeyboardController.current val signUpButtonActivated by remember( - emailInput, + usernameInput, passwordInput ) { derivedStateOf { - emailInput.isValidEmail() && passwordInput.isValidPassword() && hobbyInput.isValidHobby() + usernameInput.isValidUsername() && passwordInput.isValidPassword() && hobbyInput.isValidHobby() } } @@ -69,10 +69,10 @@ fun SignUpContentScreen( modifier = modifier .padding(top = 24.dp) .padding(horizontal = 6.dp), - emailInput = emailInput, + usernameInput = usernameInput, passwordInput = passwordInput, hobbyInput = hobbyInput, - onEmailInputChanged = onEmailInputChanged, + onUsernameInputChanged = onUsernameInputChanged, onPasswordInputChanged = onPasswordInputChanged, onHobbyInputChanged = onHobbyInputChanged ) @@ -87,7 +87,7 @@ fun SignUpContentScreen( onSignUpButtonClicked() } else { when { - emailInput.isValidEmail().not() -> toast.show() + usernameInput.isValidUsername().not() -> toast.show() passwordInput.isValidPassword().not() -> toast.show() } } @@ -97,7 +97,7 @@ fun SignUpContentScreen( LaunchedEffect(signUpButtonActivated) { when { - emailInput.isValidEmail().not() -> { + usernameInput.isValidUsername().not() -> { signUpFailureMessage = ContextCompat.getString(context, R.string.check_email_format) } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt index 252ca04..57bce7a 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt @@ -38,10 +38,10 @@ import com.sopt.presentation.ui.util.noRippleClickable @Composable fun SignUpInputContentView( modifier: Modifier = Modifier, - emailInput: String = "", + usernameInput: String = "", passwordInput: String = "", hobbyInput: String = "", - onEmailInputChanged: (String) -> Unit = {}, + onUsernameInputChanged: (String) -> Unit = {}, onPasswordInputChanged: (String) -> Unit = {}, onHobbyInputChanged: (String) -> Unit = {} ) { @@ -90,9 +90,9 @@ fun SignUpInputContentView( modifier = Modifier .padding(top = 28.dp) .fillMaxWidth(), - value = emailInput, - onValueChange = onEmailInputChanged, - placeholder = stringResource(R.string.sign_up_email_input_placeholder) + value = usernameInput, + onValueChange = onUsernameInputChanged, + placeholder = stringResource(R.string.sign_up_username_input_placeholder) ) IconFrontText( diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt index ec58b88..f1fbc2b 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt @@ -40,7 +40,7 @@ fun SignUpScreen( viewModel: SignUpViewModel = hiltViewModel(), ) { - val emailInput by viewModel.emailInput.collectAsStateWithLifecycle() + val usernameInput by viewModel.usernameInput.collectAsStateWithLifecycle() val passwordInput by viewModel.passwordInput.collectAsStateWithLifecycle() val hobbyInput by viewModel.hobbyInput.collectAsStateWithLifecycle() @@ -84,10 +84,10 @@ fun SignUpScreen( ) { SignUpContentScreen( modifier = Modifier.padding(horizontal = 12.dp), - emailInput = emailInput, + usernameInput = usernameInput, passwordInput = passwordInput, hobbyInput = hobbyInput, - onEmailInputChanged = viewModel::onEmailInputChanged, + onUsernameInputChanged = viewModel::onUsernameInputChanged, onPasswordInputChanged = viewModel::onPasswordInputChanged, onHobbyInputChanged = viewModel::onHobbyInputChanged, onSignUpButtonClicked = viewModel::signUp @@ -100,13 +100,13 @@ fun SignUpScreen( var snackbarMessage = "" when (it) { is SignUpUiState.Success -> onSignUpSuccess() - is SignUpUiState.EmailInputEmpty -> snackbarMessage = + is SignUpUiState.UsernameInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_email_input) is SignUpUiState.PasswordInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_password_input) - is SignUpUiState.InvalidEmail -> snackbarMessage = + is SignUpUiState.InvalidUsername -> snackbarMessage = ContextCompat.getString(context, R.string.not_exist_email) is SignUpUiState.InvalidPassword -> snackbarMessage = diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt index bc29fb3..60f64ae 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt @@ -17,7 +17,7 @@ class SignUpViewModel @Inject constructor( private val signUpAccountUseCase: SignUpAccountUseCase ) : ViewModel() { - val emailInput: StateFlow + val usernameInput: StateFlow field = MutableStateFlow("") val passwordInput: StateFlow field = MutableStateFlow("") @@ -27,8 +27,8 @@ class SignUpViewModel @Inject constructor( val signUpUiState: SharedFlow field = MutableSharedFlow() - fun onEmailInputChanged(email: String) { - emailInput.value = email + fun onUsernameInputChanged(username: String) { + usernameInput.value = username } fun onPasswordInputChanged(password: String) { @@ -42,16 +42,16 @@ class SignUpViewModel @Inject constructor( fun signUp() { viewModelScope.launch { signUpAccountUseCase( - emailInput.value, + usernameInput.value, passwordInput.value, hobbyInput.value ).onSuccess { signUpUiState.emit(SignUpUiState.Success) }.onFailure { when (it) { - is SignUpError.EmailInputEmpty -> signUpUiState.emit(SignUpUiState.EmailInputEmpty) + is SignUpError.UsernameInputEmpty -> signUpUiState.emit(SignUpUiState.UsernameInputEmpty) is SignUpError.PasswordInputEmpty -> signUpUiState.emit(SignUpUiState.PasswordInputEmpty) - is SignUpError.InvalidEmail -> signUpUiState.emit(SignUpUiState.InvalidEmail) + is SignUpError.InvalidUsername -> signUpUiState.emit(SignUpUiState.InvalidUsername) is SignUpError.InvalidPassword -> signUpUiState.emit(SignUpUiState.InvalidPassword) } } @@ -61,8 +61,8 @@ class SignUpViewModel @Inject constructor( sealed interface SignUpUiState { data object Success : SignUpUiState - data object EmailInputEmpty : SignUpUiState + data object UsernameInputEmpty : SignUpUiState data object PasswordInputEmpty : SignUpUiState - data object InvalidEmail : SignUpUiState + data object InvalidUsername : SignUpUiState data object InvalidPassword : SignUpUiState } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 954de7c..228716d 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ 회원가입 - wavve@example.com + Wavve 아이디 설정 로그인, 비밀번호 찾기, 알림에 사용되니 정확한 이메일을 입력해주세요. Wavve 비밀번호 설정 취미 설정 From 570fc4416b7e1eaafcaf84400ace6f645c4a7fe0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 5 Nov 2024 18:48:58 +0900 Subject: [PATCH 05/41] =?UTF-8?q?fix:=20Password,=20hobby=20validation=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- domain/src/main/java/com/sopt/domain/util/InputValidation.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/src/main/java/com/sopt/domain/util/InputValidation.kt b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt index e8167c3..08da24e 100644 --- a/domain/src/main/java/com/sopt/domain/util/InputValidation.kt +++ b/domain/src/main/java/com/sopt/domain/util/InputValidation.kt @@ -9,5 +9,5 @@ private val passwordRegex by lazy { } fun String.isValidUsername(): Boolean = this.length in 1..8 -fun String.isValidPassword(): Boolean = passwordRegex.matches(this) -fun String.isValidHobby(): Boolean = this.isNotBlank() +fun String.isValidPassword(): Boolean = this.length in 1..8 +fun String.isValidHobby(): Boolean = this.length in 1..8 From 456cb7f20c8eb47ec41e3dd34ce531dd0cbd11cf Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 5 Nov 2024 18:59:19 +0900 Subject: [PATCH 06/41] =?UTF-8?q?add:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/data/api/remote/UserApi.kt | 4 ++ .../datasource/local/UserLocalDataSource.kt | 17 ------- .../datasource/remote/UserRemoteDataSource.kt | 5 +++ .../data/repository/UserRepositoryImpl.kt | 12 +++-- .../com/sopt/data/request/SignInRequest.kt | 10 +++++ .../com/sopt/domain/exception/ErrorType.kt | 6 +-- .../sopt/domain/repository/UserRepository.kt | 6 +-- .../com/sopt/domain/usecase/SignInUseCase.kt | 6 +-- .../signin/composable/SignInContentScreen.kt | 8 ++-- .../signin/composable/SignInContentView.kt | 12 ++--- .../screen/signin/composable/SignInScreen.kt | 19 ++++---- .../signin/viewmodel/SignInViewModel.kt | 44 +++++++++---------- .../screen/signup/composable/SignUpScreen.kt | 2 +- presentation/src/main/res/values/strings.xml | 4 +- 14 files changed, 76 insertions(+), 79 deletions(-) create mode 100644 data/src/main/java/com/sopt/data/request/SignInRequest.kt diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index c3ea3df..56bbecd 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -1,5 +1,6 @@ package com.sopt.data.api.remote +import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest import retrofit2.http.Body import retrofit2.http.POST @@ -8,4 +9,7 @@ interface UserApi { @POST("user") suspend fun signUp(@Body signUpRequest: SignUpRequest) + + @POST("login") + suspend fun signIn(@Body signInRequest: SignInRequest) } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt index 641765e..566a11e 100644 --- a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt @@ -2,29 +2,12 @@ package com.sopt.data.datasource.local import android.content.SharedPreferences import com.sopt.data.UserSharedPref -import com.sopt.domain.exception.SignInError import javax.inject.Inject class UserLocalDataSource @Inject constructor( @UserSharedPref private val userSharedPreferences: SharedPreferences ) { - fun trySignIn(email: String, password: String): Result { - val savedEmail = userSharedPreferences.getString(KEY_EMAIL, null) - val savedPassword = userSharedPreferences.getString(KEY_PASSWORD, null) - - return when { - email == savedEmail && password == savedPassword -> - Result.success(Unit) - - email != savedEmail -> - Result.failure(SignInError.NotExistEmail()) - - else -> - Result.failure(SignInError.PasswordNotMatchingWithEmail()) - } - } - companion object { private const val KEY_EMAIL = "email" private const val KEY_PASSWORD = "password" diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index a8dbf2e..3bca006 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -1,6 +1,7 @@ package com.sopt.data.datasource.remote import com.sopt.data.api.remote.UserApi +import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest import javax.inject.Inject @@ -11,4 +12,8 @@ class UserRemoteDataSource @Inject constructor( suspend fun signUp(signUpRequest: SignUpRequest) { return userApi.signUp(signUpRequest) } + + suspend fun signIn(signInRequest: SignInRequest) { + return userApi.signIn(signInRequest) + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index ce6470c..d36a7c3 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -2,8 +2,8 @@ package com.sopt.data.repository import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource +import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest -import com.sopt.domain.model.User import com.sopt.domain.repository.UserRepository import javax.inject.Inject @@ -18,11 +18,9 @@ class UserRepositoryImpl @Inject constructor( } } - override fun trySignIn(email: String, password: String): Result { - return userLocalDataSource.trySignIn(email, password) - } - - override suspend fun fetchUser(): Result { - TODO("Not yet implemented") + override suspend fun signIn(username: String, password: String): Result { + return runCatching { + userRemoteDataSource.signIn(SignInRequest(username, password)) + } } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/request/SignInRequest.kt b/data/src/main/java/com/sopt/data/request/SignInRequest.kt new file mode 100644 index 0000000..f41e48f --- /dev/null +++ b/data/src/main/java/com/sopt/data/request/SignInRequest.kt @@ -0,0 +1,10 @@ +package com.sopt.data.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SignInRequest( + @SerialName("username") val username: String, + @SerialName("password") val password: String +) diff --git a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt b/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt index f2f8801..0794335 100644 --- a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt +++ b/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt @@ -1,9 +1,9 @@ package com.sopt.domain.exception sealed class SignInError : Throwable() { - class NotExistEmail() : SignInError() - class PasswordNotMatchingWithEmail() : SignInError() - class EmailInputEmpty() : SignInError() + class NotExistUsername() : SignInError() + class PasswordNotMatchingWithUsername() : SignInError() + class UsernameInputEmpty() : SignInError() class PasswordInputEmpty() : SignInError() } diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index e4b57f4..850baf7 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -1,10 +1,6 @@ package com.sopt.domain.repository -import com.sopt.domain.model.User - interface UserRepository { suspend fun signUp(username: String, password: String, hobby: String): Result - fun trySignIn(email: String, password: String): Result - - suspend fun fetchUser(): Result + suspend fun signIn(username: String, password: String): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt index 4ef0e22..6010233 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt @@ -8,12 +8,12 @@ class SignInUseCase @Inject constructor( private val userRepository: UserRepository ) { - operator fun invoke(email: String, password: String): Result { + operator fun invoke(username: String, password: String): Result { return runCatching { when { - email.isBlank() -> Result.failure(SignInError.EmailInputEmpty()) + username.isBlank() -> Result.failure(SignInError.UsernameInputEmpty()) password.isBlank() -> Result.failure(SignInError.PasswordInputEmpty()) - else -> userRepository.trySignIn(email, password) + else -> userRepository.signIn(username, password) } } } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentScreen.kt index 3e5ab46..5b26aef 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentScreen.kt @@ -14,9 +14,9 @@ fun SignInContentScreen( modifier: Modifier = Modifier, onSignInButtonClicked: () -> Unit, onNavigateToSignUp: () -> Unit, - emailInput: String, + usernameInput: String, passwordInput: String, - onEmailInputChanged: (String) -> Unit, + onUsernameInputChanged: (String) -> Unit, onPasswordInputChanged: (String) -> Unit ) { @@ -27,9 +27,9 @@ fun SignInContentScreen( ) { SignInContentView( modifier = Modifier.padding(top = 36.dp), - emailInput = emailInput, + usernameInput = usernameInput, passwordInput = passwordInput, - onEmailInputChanged = onEmailInputChanged, + onUsernameInputChanged = onUsernameInputChanged, onPasswordInputChanged = onPasswordInputChanged, onSignInButtonClicked = onSignInButtonClicked, onNavigateToSignUp = onNavigateToSignUp diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentView.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentView.kt index 4c2e526..9bd41b6 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentView.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInContentView.kt @@ -36,9 +36,9 @@ import com.sopt.presentation.ui.util.noRippleClickable @Composable fun SignInContentView( modifier: Modifier = Modifier, - emailInput: String, + usernameInput: String, passwordInput: String, - onEmailInputChanged: (String) -> Unit, + onUsernameInputChanged: (String) -> Unit, onPasswordInputChanged: (String) -> Unit, onSignInButtonClicked: () -> Unit, onNavigateToSignUp: () -> Unit @@ -50,9 +50,9 @@ fun SignInContentView( modifier = modifier ) { FilledTextField( - value = emailInput, + value = usernameInput, innerPadding = PaddingValues(horizontal = 12.dp, vertical = 18.dp), - onValueChange = onEmailInputChanged, + onValueChange = onUsernameInputChanged, placeholder = stringResource(R.string.email_address_or_id), ) FilledTextField( @@ -155,9 +155,9 @@ fun SignInContentView( @Composable private fun SignInContentViewPreview() { SignInContentView( - emailInput = "", + usernameInput = "", passwordInput = "", - onEmailInputChanged = {}, + onUsernameInputChanged = {}, onPasswordInputChanged = {}, onSignInButtonClicked = {}, onNavigateToSignUp = {} diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt index 6317b7e..095186f 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -43,8 +44,8 @@ fun SignInScreen( val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current - val emailInput = viewModel.emailInput.collectAsStateWithLifecycle() - val passwordInput = viewModel.passwordInput.collectAsStateWithLifecycle() + val usernameInput by viewModel.usernameInput.collectAsStateWithLifecycle() + val passwordInput by viewModel.passwordInput.collectAsStateWithLifecycle() Scaffold( modifier = modifier, @@ -82,9 +83,9 @@ fun SignInScreen( .padding(horizontal = 14.dp), onSignInButtonClicked = viewModel::trySignIn, onNavigateToSignUp = onNavigateToSignUp, - emailInput = emailInput.value, - passwordInput = passwordInput.value, - onEmailInputChanged = viewModel::onEmailInputChanged, + usernameInput = usernameInput, + passwordInput = passwordInput, + onUsernameInputChanged = viewModel::onUsernameInputChanged, onPasswordInputChanged = viewModel::onPasswordInputChanged ) } @@ -95,16 +96,16 @@ fun SignInScreen( var snackbarMessage = "" when (it) { is SignInUiState.Success -> onSignInSuccess() - is SignInUiState.EmailInputEmpty -> snackbarMessage = - ContextCompat.getString(context, R.string.require_email_input) + is SignInUiState.UsernameInputEmpty -> snackbarMessage = + ContextCompat.getString(context, R.string.require_username_input) is SignInUiState.PasswordInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_password_input) - is SignInUiState.NotExistEmail -> snackbarMessage = + is SignInUiState.NotExistUsername -> snackbarMessage = ContextCompat.getString(context, R.string.not_exist_email) - is SignInUiState.PasswordNotMatchingWithEmail -> snackbarMessage = + is SignInUiState.PasswordNotMatchingWithUsername -> snackbarMessage = ContextCompat.getString(context, R.string.not_exist_password) } if (it !is SignInUiState.Success) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt index f958a1b..a424ac4 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt @@ -7,9 +7,9 @@ import com.sopt.domain.usecase.SignInUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -17,20 +17,16 @@ class SignInViewModel @Inject constructor( private val signInUseCase: SignInUseCase ) : ViewModel() { - val emailInput: StateFlow + val usernameInput: StateFlow field = MutableStateFlow("") val passwordInput: StateFlow field = MutableStateFlow("") - private val _signInUiState = - MutableSharedFlow(extraBufferCapacity = 1) - val signInUiState = _signInUiState.shareIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000) - ) + val signInUiState: SharedFlow + field = MutableSharedFlow() - fun onEmailInputChanged(email: String) { - emailInput.value = email + fun onUsernameInputChanged(username: String) { + usernameInput.value = username } fun onPasswordInputChanged(password: String) { @@ -38,14 +34,18 @@ class SignInViewModel @Inject constructor( } fun trySignIn() { - signInUseCase(emailInput.value, passwordInput.value).onSuccess { - _signInUiState.tryEmit(SignInUiState.Success) - }.onFailure { - when (it) { - is SignInError.EmailInputEmpty -> _signInUiState.tryEmit(SignInUiState.EmailInputEmpty) - is SignInError.PasswordInputEmpty -> _signInUiState.tryEmit(SignInUiState.PasswordInputEmpty) - is SignInError.NotExistEmail -> _signInUiState.tryEmit(SignInUiState.NotExistEmail) - is SignInError.PasswordNotMatchingWithEmail -> _signInUiState.tryEmit(SignInUiState.PasswordNotMatchingWithEmail) + viewModelScope.launch { + signInUseCase(usernameInput.value, passwordInput.value).onSuccess { + signInUiState.emit(SignInUiState.Success) + }.onFailure { + when (it) { + is SignInError.UsernameInputEmpty -> signInUiState.emit(SignInUiState.UsernameInputEmpty) + is SignInError.PasswordInputEmpty -> signInUiState.emit(SignInUiState.PasswordInputEmpty) + is SignInError.NotExistUsername -> signInUiState.emit(SignInUiState.NotExistUsername) + is SignInError.PasswordNotMatchingWithUsername -> signInUiState.emit( + SignInUiState.PasswordNotMatchingWithUsername + ) + } } } } @@ -53,8 +53,8 @@ class SignInViewModel @Inject constructor( sealed interface SignInUiState { data object Success : SignInUiState - data object EmailInputEmpty : SignInUiState + data object UsernameInputEmpty : SignInUiState data object PasswordInputEmpty : SignInUiState - data object NotExistEmail : SignInUiState - data object PasswordNotMatchingWithEmail : SignInUiState + data object NotExistUsername : SignInUiState + data object PasswordNotMatchingWithUsername : SignInUiState } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt index f1fbc2b..3f55e3d 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt @@ -101,7 +101,7 @@ fun SignUpScreen( when (it) { is SignUpUiState.Success -> onSignUpSuccess() is SignUpUiState.UsernameInputEmpty -> snackbarMessage = - ContextCompat.getString(context, R.string.require_email_input) + ContextCompat.getString(context, R.string.require_username_input) is SignUpUiState.PasswordInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_password_input) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 228716d..25781d0 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -58,9 +58,9 @@ 또는 다른 서비스 계정으로 로그인 로그인 성공 ! 로그인 실패 - 이메일을 입력해주세요. + 아이디를 입력해주세요. 비밀번호를 입력해주세요. - 존재하지 않는 이메일입니다. + 존재하지 않는 아이디입니다. 비밀번호가 일치하지 않습니다. From 166364ca890d97baf761564413d143f9ca3441d4 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:16:42 +0900 Subject: [PATCH 07/41] add: result unwrap call adapter factory --- .../sopt/and/adapter/NetworkCallAdapter.kt | 49 +++++++++++++++++++ .../java/org/sopt/and/di/NetworkModule.kt | 2 + 2 files changed, 51 insertions(+) create mode 100644 app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt diff --git a/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt b/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt new file mode 100644 index 0000000..2ce3c29 --- /dev/null +++ b/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt @@ -0,0 +1,49 @@ +package org.sopt.and.adapter + +import kotlinx.serialization.json.JsonObject +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ResultCallAdapterFactory : CallAdapter.Factory() { + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (getRawType(returnType) != Call::class.java) return null + + val responseType = (returnType as? ParameterizedType)?.actualTypeArguments?.firstOrNull() + ?: return null + + return object : CallAdapter> { + override fun responseType(): Type = responseType + + override fun adapt(call: Call): Call { + return ResultCall(call) + } + } + } +} + +private class ResultCall( + private val delegate: Call +) : Call by delegate { + + override fun enqueue(callback: Callback) = delegate.enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + val result = (response.body() as? JsonObject)?.get("result") + callback.onResponse(this@ResultCall, Response.success(result)) + } + + override fun onFailure(call: Call, t: Throwable) { + callback.onFailure(call, t) + } + } + ) +} diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt index 4d13ceb..dcd3deb 100644 --- a/app/src/main/java/org/sopt/and/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -8,6 +8,7 @@ import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import org.sopt.and.BuildConfig +import org.sopt.and.adapter.ResultCallAdapterFactory import retrofit2.Retrofit import javax.inject.Singleton @@ -21,6 +22,7 @@ object NetworkModule { return Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .addCallAdapterFactory(ResultCallAdapterFactory()) .build() } } \ No newline at end of file From c38fd5f2e8cc302d5aa9e03c3d47e7a88fa562e0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:17:47 +0900 Subject: [PATCH 08/41] fix: suspend --- domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt index 6010233..9d8fa17 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt @@ -8,7 +8,7 @@ class SignInUseCase @Inject constructor( private val userRepository: UserRepository ) { - operator fun invoke(username: String, password: String): Result { + suspend operator fun invoke(username: String, password: String): Result { return runCatching { when { username.isBlank() -> Result.failure(SignInError.UsernameInputEmpty()) From 6cf6f0e7d1843de9ab701be8faba491d8be5430f Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:24:51 +0900 Subject: [PATCH 09/41] add: EncryptedSharedPref dependency --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0c689a9..ec33895 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,4 +84,7 @@ dependencies { implementation(libs.retrofit) implementation(libs.retrofit.kotlin.serialization.converter) implementation(libs.kotlinx.serialization.json) + + implementation(libs.androidx.security.crypto.ktx) + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1565675..ea76cc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,10 +20,12 @@ navigationCompose = "2.8.2" okhttp = "4.11.0" retrofit = "2.11.0" retrofitKotlinSerializationConverter = "1.0.0" +securityCryptoKtx = "1.1.0-alpha06" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" } glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } From df71590e002bc9611a4dfdc2adbab2a6915e4440 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:30:38 +0900 Subject: [PATCH 10/41] =?UTF-8?q?add:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/and/di/LocalApiModule.kt | 26 ++++++++++++++++++- .../src/main/java/com/sopt/data/Qualifiers.kt | 6 ++++- .../java/com/sopt/data/api/remote/UserApi.kt | 6 +++-- .../datasource/local/UserLocalDataSource.kt | 13 +++++++--- .../datasource/remote/UserRemoteDataSource.kt | 6 +++-- .../java/com/sopt/data/dto/user/SignInDto.kt | 9 +++++++ .../java/com/sopt/data/dto/user/SignUpDto.kt | 9 +++++++ .../data/repository/UserRepositoryImpl.kt | 3 ++- 8 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 data/src/main/java/com/sopt/data/dto/user/SignInDto.kt create mode 100644 data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt diff --git a/app/src/main/java/org/sopt/and/di/LocalApiModule.kt b/app/src/main/java/org/sopt/and/di/LocalApiModule.kt index bf07fe7..d6be321 100644 --- a/app/src/main/java/org/sopt/and/di/LocalApiModule.kt +++ b/app/src/main/java/org/sopt/and/di/LocalApiModule.kt @@ -2,6 +2,9 @@ package org.sopt.and.di import android.content.Context import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.sopt.data.UserEncryptedSharedPref import com.sopt.data.UserSharedPref import dagger.Module import dagger.Provides @@ -17,9 +20,30 @@ object LocalApiModule { @UserSharedPref @Provides @Singleton - fun provideUserLocalDataSource( + fun provideUserSharedPreferences( @ApplicationContext context: Context ): SharedPreferences { return context.getSharedPreferences("user.pref", Context.MODE_PRIVATE) } + + @UserEncryptedSharedPref + @Provides + @Singleton + fun provideUserEncryptedSharedPreferences( + @ApplicationContext context: Context + ): SharedPreferences { + val masterKeyAlias = MasterKey + .Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + + return EncryptedSharedPreferences.create( + context, + "user.pref.enc", + masterKeyAlias, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/Qualifiers.kt b/data/src/main/java/com/sopt/data/Qualifiers.kt index 3d36c6d..36edc13 100644 --- a/data/src/main/java/com/sopt/data/Qualifiers.kt +++ b/data/src/main/java/com/sopt/data/Qualifiers.kt @@ -4,4 +4,8 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class UserSharedPref \ No newline at end of file +annotation class UserSharedPref + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class UserEncryptedSharedPref \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index 56bbecd..4b4e2d7 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -1,5 +1,7 @@ package com.sopt.data.api.remote +import com.sopt.data.dto.user.SignInDto +import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest import retrofit2.http.Body @@ -8,8 +10,8 @@ import retrofit2.http.POST interface UserApi { @POST("user") - suspend fun signUp(@Body signUpRequest: SignUpRequest) + suspend fun signUp(@Body signUpRequest: SignUpRequest): SignUpDto @POST("login") - suspend fun signIn(@Body signInRequest: SignInRequest) + suspend fun signIn(@Body signInRequest: SignInRequest): SignInDto } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt index 566a11e..986142c 100644 --- a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt @@ -1,15 +1,22 @@ package com.sopt.data.datasource.local import android.content.SharedPreferences +import com.sopt.data.UserEncryptedSharedPref import com.sopt.data.UserSharedPref import javax.inject.Inject class UserLocalDataSource @Inject constructor( - @UserSharedPref private val userSharedPreferences: SharedPreferences + @UserSharedPref private val userSharedPreferences: SharedPreferences, + @UserEncryptedSharedPref private val userEncryptedSharedPref: SharedPreferences ) { + fun saveToken(token: String) { + userEncryptedSharedPref.edit() + .putString(TOKEN, token) + .apply() + } + companion object { - private const val KEY_EMAIL = "email" - private const val KEY_PASSWORD = "password" + private const val TOKEN = "token" } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index 3bca006..3ccfd79 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -1,6 +1,8 @@ package com.sopt.data.datasource.remote import com.sopt.data.api.remote.UserApi +import com.sopt.data.dto.user.SignInDto +import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest import javax.inject.Inject @@ -9,11 +11,11 @@ class UserRemoteDataSource @Inject constructor( private val userApi: UserApi ) { - suspend fun signUp(signUpRequest: SignUpRequest) { + suspend fun signUp(signUpRequest: SignUpRequest): SignUpDto { return userApi.signUp(signUpRequest) } - suspend fun signIn(signInRequest: SignInRequest) { + suspend fun signIn(signInRequest: SignInRequest): SignInDto { return userApi.signIn(signInRequest) } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt b/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt new file mode 100644 index 0000000..f305871 --- /dev/null +++ b/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt @@ -0,0 +1,9 @@ +package com.sopt.data.dto.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SignInDto( + @SerialName("token") val token: String +) diff --git a/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt new file mode 100644 index 0000000..01720d7 --- /dev/null +++ b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt @@ -0,0 +1,9 @@ +package com.sopt.data.dto.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SignUpDto( + @SerialName("no") val no: Int +) diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index d36a7c3..e46f805 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -20,7 +20,8 @@ class UserRepositoryImpl @Inject constructor( override suspend fun signIn(username: String, password: String): Result { return runCatching { - userRemoteDataSource.signIn(SignInRequest(username, password)) + val signInDto = userRemoteDataSource.signIn(SignInRequest(username, password)) + userLocalDataSource.saveToken(signInDto.token) } } } \ No newline at end of file From 1bdaa012f4b338237ad239a253425d3a38abd999 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:38:29 +0900 Subject: [PATCH 11/41] =?UTF-8?q?refactor:=20User=20->=20TokenLocalDataSou?= =?UTF-8?q?rce=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20token=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/and/di/LocalApiModule.kt | 9 ++++--- .../java/org/sopt/and/di/NetworkModule.kt | 18 ++++++++++++++ .../src/main/java/com/sopt/data/Qualifiers.kt | 2 +- .../datasource/local/TokenLocalDataSource.kt | 24 +++++++++++++++++++ .../datasource/local/UserLocalDataSource.kt | 11 --------- .../data/repository/UserRepositoryImpl.kt | 6 +++-- 6 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 data/src/main/java/com/sopt/data/datasource/local/TokenLocalDataSource.kt diff --git a/app/src/main/java/org/sopt/and/di/LocalApiModule.kt b/app/src/main/java/org/sopt/and/di/LocalApiModule.kt index d6be321..d4aff86 100644 --- a/app/src/main/java/org/sopt/and/di/LocalApiModule.kt +++ b/app/src/main/java/org/sopt/and/di/LocalApiModule.kt @@ -4,7 +4,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -import com.sopt.data.UserEncryptedSharedPref +import com.sopt.data.TokenEncryptedSharedPref import com.sopt.data.UserSharedPref import dagger.Module import dagger.Provides @@ -26,10 +26,10 @@ object LocalApiModule { return context.getSharedPreferences("user.pref", Context.MODE_PRIVATE) } - @UserEncryptedSharedPref + @TokenEncryptedSharedPref @Provides @Singleton - fun provideUserEncryptedSharedPreferences( + fun provideTokenEncryptedSharedPreferences( @ApplicationContext context: Context ): SharedPreferences { val masterKeyAlias = MasterKey @@ -37,10 +37,9 @@ object LocalApiModule { .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() - return EncryptedSharedPreferences.create( context, - "user.pref.enc", + "token.pref.enc", masterKeyAlias, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt index dcd3deb..52dd42e 100644 --- a/app/src/main/java/org/sopt/and/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -1,12 +1,16 @@ package org.sopt.and.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.sopt.data.datasource.local.TokenLocalDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request import org.sopt.and.BuildConfig import org.sopt.and.adapter.ResultCallAdapterFactory import retrofit2.Retrofit @@ -16,6 +20,20 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { + @Provides + @Singleton + fun provideAuthInterceptor( + tokenLocalDataSource: TokenLocalDataSource, + ): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + val token = runBlocking { tokenLocalDataSource.getToken() } + val newRequest: Request = chain.request().newBuilder() + .addHeader("token", token) + .build() + chain.proceed(newRequest) + } + } + @Singleton @Provides fun provideRetrofit(): Retrofit { diff --git a/data/src/main/java/com/sopt/data/Qualifiers.kt b/data/src/main/java/com/sopt/data/Qualifiers.kt index 36edc13..d8b3b59 100644 --- a/data/src/main/java/com/sopt/data/Qualifiers.kt +++ b/data/src/main/java/com/sopt/data/Qualifiers.kt @@ -8,4 +8,4 @@ annotation class UserSharedPref @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class UserEncryptedSharedPref \ No newline at end of file +annotation class TokenEncryptedSharedPref \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/local/TokenLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/TokenLocalDataSource.kt new file mode 100644 index 0000000..17d85fe --- /dev/null +++ b/data/src/main/java/com/sopt/data/datasource/local/TokenLocalDataSource.kt @@ -0,0 +1,24 @@ +package com.sopt.data.datasource.local + +import android.content.SharedPreferences +import com.sopt.data.TokenEncryptedSharedPref +import javax.inject.Inject + +class TokenLocalDataSource @Inject constructor( + @TokenEncryptedSharedPref private val tokenEncryptedSharedPref: SharedPreferences +) { + + fun saveToken(token: String) { + tokenEncryptedSharedPref.edit() + .putString(TOKEN, token) + .apply() + } + + fun getToken(): String { + return tokenEncryptedSharedPref.getString(TOKEN, "").orEmpty() + } + + companion object { + private const val TOKEN = "token" + } +} \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt index 986142c..893155b 100644 --- a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt @@ -1,22 +1,11 @@ package com.sopt.data.datasource.local import android.content.SharedPreferences -import com.sopt.data.UserEncryptedSharedPref import com.sopt.data.UserSharedPref import javax.inject.Inject class UserLocalDataSource @Inject constructor( @UserSharedPref private val userSharedPreferences: SharedPreferences, - @UserEncryptedSharedPref private val userEncryptedSharedPref: SharedPreferences ) { - fun saveToken(token: String) { - userEncryptedSharedPref.edit() - .putString(TOKEN, token) - .apply() - } - - companion object { - private const val TOKEN = "token" - } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index e46f805..82f8ca4 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.sopt.data.repository +import com.sopt.data.datasource.local.TokenLocalDataSource import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest @@ -9,7 +10,8 @@ import javax.inject.Inject class UserRepositoryImpl @Inject constructor( private val userLocalDataSource: UserLocalDataSource, - private val userRemoteDataSource: UserRemoteDataSource + private val userRemoteDataSource: UserRemoteDataSource, + private val tokenLocalDataSource: TokenLocalDataSource ) : UserRepository { override suspend fun signUp(username: String, password: String, hobby: String): Result { @@ -21,7 +23,7 @@ class UserRepositoryImpl @Inject constructor( override suspend fun signIn(username: String, password: String): Result { return runCatching { val signInDto = userRemoteDataSource.signIn(SignInRequest(username, password)) - userLocalDataSource.saveToken(signInDto.token) + tokenLocalDataSource.saveToken(signInDto.token) } } } \ No newline at end of file From 3f79f7a201d6fcb456e0d215117ad89a985690a8 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:48:34 +0900 Subject: [PATCH 12/41] =?UTF-8?q?fix:=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=A0=9C=EA=B1=B0,=20=EB=82=B4=20=EC=B7=A8?= =?UTF-8?q?=EB=AF=B8=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/sopt/and/di/NetworkModule.kt | 18 ------------------ .../java/com/sopt/data/api/remote/UserApi.kt | 6 ++++++ .../java/com/sopt/data/dto/user/HobbyDto.kt | 9 +++++++++ .../java/com/sopt/data/dto/user/SignInDto.kt | 2 +- .../java/com/sopt/data/dto/user/SignUpDto.kt | 2 +- .../sopt/data/repository/UserRepositoryImpl.kt | 2 +- 6 files changed, 18 insertions(+), 21 deletions(-) create mode 100644 data/src/main/java/com/sopt/data/dto/user/HobbyDto.kt diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt index 52dd42e..dcd3deb 100644 --- a/app/src/main/java/org/sopt/and/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -1,16 +1,12 @@ package org.sopt.and.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import com.sopt.data.datasource.local.TokenLocalDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType -import okhttp3.Request import org.sopt.and.BuildConfig import org.sopt.and.adapter.ResultCallAdapterFactory import retrofit2.Retrofit @@ -20,20 +16,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - @Provides - @Singleton - fun provideAuthInterceptor( - tokenLocalDataSource: TokenLocalDataSource, - ): Interceptor { - return Interceptor { chain: Interceptor.Chain -> - val token = runBlocking { tokenLocalDataSource.getToken() } - val newRequest: Request = chain.request().newBuilder() - .addHeader("token", token) - .build() - chain.proceed(newRequest) - } - } - @Singleton @Provides fun provideRetrofit(): Retrofit { diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index 4b4e2d7..3a8930c 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -1,10 +1,13 @@ package com.sopt.data.api.remote +import com.sopt.data.dto.user.HobbyDto import com.sopt.data.dto.user.SignInDto import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.POST interface UserApi { @@ -14,4 +17,7 @@ interface UserApi { @POST("login") suspend fun signIn(@Body signInRequest: SignInRequest): SignInDto + + @GET("user/my-hobby") + suspend fun getMyHobby(@Header("token") token: String): HobbyDto } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/dto/user/HobbyDto.kt b/data/src/main/java/com/sopt/data/dto/user/HobbyDto.kt new file mode 100644 index 0000000..d40f0ae --- /dev/null +++ b/data/src/main/java/com/sopt/data/dto/user/HobbyDto.kt @@ -0,0 +1,9 @@ +package com.sopt.data.dto.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HobbyDto( + @SerialName("hobby") val hobby: String? +) \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt b/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt index f305871..b1b26fd 100644 --- a/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt +++ b/data/src/main/java/com/sopt/data/dto/user/SignInDto.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class SignInDto( - @SerialName("token") val token: String + @SerialName("token") val token: String? ) diff --git a/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt index 01720d7..4a4f63b 100644 --- a/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt +++ b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class SignUpDto( - @SerialName("no") val no: Int + @SerialName("no") val no: Int? ) diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 82f8ca4..4a138a9 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -23,7 +23,7 @@ class UserRepositoryImpl @Inject constructor( override suspend fun signIn(username: String, password: String): Result { return runCatching { val signInDto = userRemoteDataSource.signIn(SignInRequest(username, password)) - tokenLocalDataSource.saveToken(signInDto.token) + signInDto.token?.let { tokenLocalDataSource.saveToken(it) } } } } \ No newline at end of file From 62c4df2904d4cc105dd17f6fb93985822cd91780 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:53:48 +0900 Subject: [PATCH 13/41] =?UTF-8?q?add:=20=EB=82=B4=20=EC=B7=A8=EB=AF=B8=20a?= =?UTF-8?q?pi=20=ED=98=B8=EC=B6=9C=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/remote/UserRemoteDataSource.kt | 5 +++ .../data/repository/UserRepositoryImpl.kt | 7 +++++ .../sopt/domain/repository/UserRepository.kt | 1 + .../ui/screen/my/viewmodel/MyViewModel.kt | 31 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index 3ccfd79..2ffa5b1 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -1,6 +1,7 @@ package com.sopt.data.datasource.remote import com.sopt.data.api.remote.UserApi +import com.sopt.data.dto.user.HobbyDto import com.sopt.data.dto.user.SignInDto import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest @@ -18,4 +19,8 @@ class UserRemoteDataSource @Inject constructor( suspend fun signIn(signInRequest: SignInRequest): SignInDto { return userApi.signIn(signInRequest) } + + suspend fun getMyHobby(token: String): HobbyDto { + return userApi.getMyHobby(token) + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 4a138a9..94674e0 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -26,4 +26,11 @@ class UserRepositoryImpl @Inject constructor( signInDto.token?.let { tokenLocalDataSource.saveToken(it) } } } + + override suspend fun getMyHobby(): Result { + return runCatching { + val token = tokenLocalDataSource.getToken() + userRemoteDataSource.getMyHobby(token).hobby ?: "" + } + } } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index 850baf7..adfbd9b 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -3,4 +3,5 @@ package com.sopt.domain.repository interface UserRepository { suspend fun signUp(username: String, password: String, hobby: String): Result suspend fun signIn(username: String, password: String): Result + suspend fun getMyHobby(): Result } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt new file mode 100644 index 0000000..806d171 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt @@ -0,0 +1,31 @@ +package com.sopt.presentation.ui.screen.my.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sopt.domain.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transform +import javax.inject.Inject + +@HiltViewModel +class MyViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + + val myHobby = flow { + emit(userRepository.getMyHobby()) + }.transform { result -> + result.onSuccess { + emit(it) + }.onFailure { + emit("Unknown") + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = "" + ) +} \ No newline at end of file From 49e7937206606f5776f8acc00fcf469d7e39a306 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Wed, 6 Nov 2024 15:56:33 +0900 Subject: [PATCH 14/41] =?UTF-8?q?add:=20=EB=82=B4=20=EC=B7=A8=EB=AF=B8=20?= =?UTF-8?q?=EB=B7=B0=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sopt/presentation/User.kt | 9 ------ .../screen/my/composable/MyContentScreen.kt | 32 +++++++++++++------ .../ui/screen/my/composable/MyScreen.kt | 15 +++++++-- 3 files changed, 35 insertions(+), 21 deletions(-) delete mode 100644 presentation/src/main/java/com/sopt/presentation/User.kt diff --git a/presentation/src/main/java/com/sopt/presentation/User.kt b/presentation/src/main/java/com/sopt/presentation/User.kt deleted file mode 100644 index a2cb1d5..0000000 --- a/presentation/src/main/java/com/sopt/presentation/User.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sopt.presentation - -/** - * 1주차 과제 수행을 위한 임시 User 클래스 - */ -object User { - var email = "" - var password = "" -} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt index 0d59170..cf32973 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.presentation.R -import com.sopt.presentation.User import com.sopt.presentation.ui.component.box.NoContentAlertBox import com.sopt.presentation.ui.component.icon.PrimaryIcon import com.sopt.presentation.ui.component.image.CircularImage @@ -29,7 +28,8 @@ import com.sopt.presentation.ui.theme.WavveTheme @Composable fun MyContentScreen( - modifier: Modifier = Modifier + myHobby: String, + modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -49,7 +49,7 @@ fun MyContentScreen( ) PrimaryText( modifier = Modifier.padding(start = 12.dp), - text = User.email, + text = myHobby, style = WavveTheme.typography.bodyLarge, ) Spacer(modifier = Modifier.weight(1f)) @@ -83,16 +83,20 @@ fun MyContentScreen( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = stringResource(R.string.profile_image), - ) + ) } } } VariantSurface( - modifier = Modifier.padding(top = 1.dp).fillMaxWidth() + modifier = Modifier + .padding(top = 1.dp) + .fillMaxWidth() ) { Column( - modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp, bottom = 20.dp) + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp, bottom = 20.dp) ) { SecondaryText( text = stringResource(R.string.have_no_wavve_access), @@ -116,14 +120,18 @@ fun MyContentScreen( } Column( - modifier = Modifier.padding(16.dp).fillMaxWidth() + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() ) { PrimaryText( text = stringResource(R.string.total_viewing_history), style = WavveTheme.typography.headSmall ) NoContentAlertBox( - modifier = Modifier.fillMaxWidth().padding(vertical = 60.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 60.dp), text = stringResource(R.string.no_viewing_history) ) @@ -132,7 +140,9 @@ fun MyContentScreen( style = WavveTheme.typography.headSmall ) NoContentAlertBox( - modifier = Modifier.fillMaxWidth().padding(vertical = 60.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 60.dp), text = stringResource(R.string.no_interested_program) ) } @@ -142,5 +152,7 @@ fun MyContentScreen( @Composable @Preview private fun MyContentScreenPreview() { - MyContentScreen() + MyContentScreen( + myHobby = "축구", + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt index be7ec75..17998e4 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt @@ -4,18 +4,29 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.presentation.ui.component.surface.DefaultSurface +import com.sopt.presentation.ui.screen.my.viewmodel.MyViewModel @Composable -fun MyScreen(modifier: Modifier = Modifier) { +fun MyScreen( + modifier: Modifier = Modifier, + viewModel: MyViewModel = hiltViewModel() +) { + + val myHobby by viewModel.myHobby.collectAsStateWithLifecycle() + DefaultSurface( modifier = modifier .fillMaxSize() ) { MyContentScreen( modifier = Modifier - .fillMaxSize() + .fillMaxSize(), + myHobby = myHobby ) } } \ No newline at end of file From 15caab8422870047d6394ea5b04dc49d6d1e7cbe Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:53:12 +0900 Subject: [PATCH 15/41] add: Success Response Converter --- .../sopt/and/adapter/NetworkCallAdapter.kt | 65 ++++++++----------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt b/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt index 2ce3c29..48a4d1d 100644 --- a/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt +++ b/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt @@ -1,49 +1,36 @@ package org.sopt.and.adapter -import kotlinx.serialization.json.JsonObject -import retrofit2.Call -import retrofit2.CallAdapter -import retrofit2.Callback -import retrofit2.Response +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.serializer +import okhttp3.ResponseBody +import retrofit2.Converter import retrofit2.Retrofit -import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -class ResultCallAdapterFactory : CallAdapter.Factory() { - override fun get( - returnType: Type, - annotations: Array, - retrofit: Retrofit - ): CallAdapter<*, *>? { - if (getRawType(returnType) != Call::class.java) return null - - val responseType = (returnType as? ParameterizedType)?.actualTypeArguments?.firstOrNull() - ?: return null - - return object : CallAdapter> { - override fun responseType(): Type = responseType - - override fun adapt(call: Call): Call { - return ResultCall(call) - } +class ResultConverter( + private val serializer: KSerializer, + private val json: Json +) : Converter { + override fun convert(responseBody: ResponseBody): T? { + return responseBody.use { + val jsonObject = json.parseToJsonElement(responseBody.string()).jsonObject + val result = jsonObject["result"] ?: return null + json.decodeFromJsonElement(serializer, result) } } } -private class ResultCall( - private val delegate: Call -) : Call by delegate { - - override fun enqueue(callback: Callback) = delegate.enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - val result = (response.body() as? JsonObject)?.get("result") - callback.onResponse(this@ResultCall, Response.success(result)) - } +class ResultConverterFactory : Converter.Factory() { - override fun onFailure(call: Call, t: Throwable) { - callback.onFailure(call, t) - } - } - ) -} + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit + ): Converter { + val json = Json { ignoreUnknownKeys = true } + val resultSerializer = json.serializersModule.serializer(type) + return ResultConverter(resultSerializer, json) + } +} \ No newline at end of file From 90e1ea61fdf017b6eb7b302db88b2f41406066ba Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:54:46 +0900 Subject: [PATCH 16/41] add: Error response parsing --- .../java/org/sopt/and/di/NetworkModule.kt | 64 +++++++++++++++++-- .../{ErrorType.kt => NetworkError.kt} | 11 ++++ 2 files changed, 70 insertions(+), 5 deletions(-) rename domain/src/main/java/com/sopt/domain/exception/{ErrorType.kt => NetworkError.kt} (63%) diff --git a/app/src/main/java/org/sopt/and/di/NetworkModule.kt b/app/src/main/java/org/sopt/and/di/NetworkModule.kt index dcd3deb..9557077 100644 --- a/app/src/main/java/org/sopt/and/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/and/di/NetworkModule.kt @@ -1,15 +1,22 @@ package org.sopt.and.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.sopt.domain.exception.NetworkError import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import org.sopt.and.BuildConfig -import org.sopt.and.adapter.ResultCallAdapterFactory +import org.sopt.and.adapter.ResultConverterFactory import retrofit2.Retrofit +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @@ -18,11 +25,58 @@ object NetworkModule { @Singleton @Provides - fun provideRetrofit(): Retrofit { + fun provideClient( + responseInterceptor: Interceptor + ): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }).addInterceptor(responseInterceptor) + .build() + } + + @Singleton + @Provides + fun provideResponseInterceptor( + ) : Interceptor = Interceptor { chain -> + val response = chain.proceed(chain.request()) + + if (response.isSuccessful.not()) { + val errorBody = response.body?.string() + val errorResponse = try { + Json.decodeFromString(errorBody ?: "") + } catch (e: Exception) { + null + } + + throw NetworkError( + statusCode = response.code, + errorCode = errorResponse?.code?.toInt() ?: -1, + message = response.message, + ) + } + return@Interceptor response + } + + @Singleton + @Provides + fun provideRetrofit( + client: OkHttpClient + ): Retrofit { + val json = Json { ignoreUnknownKeys = true } return Retrofit.Builder() .baseUrl(BuildConfig.BASE_URL) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .addCallAdapterFactory(ResultCallAdapterFactory()) + .client(client) + .addConverterFactory(ResultConverterFactory()) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() } -} \ No newline at end of file +} + +@Serializable +private data class ErrorResponse( + val code: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt similarity index 63% rename from domain/src/main/java/com/sopt/domain/exception/ErrorType.kt rename to domain/src/main/java/com/sopt/domain/exception/NetworkError.kt index 0794335..473089a 100644 --- a/domain/src/main/java/com/sopt/domain/exception/ErrorType.kt +++ b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt @@ -1,5 +1,13 @@ package com.sopt.domain.exception +import java.io.IOException + +data class NetworkError( + val statusCode: Int, + val errorCode: Int, + override val message: String? +) : IOException() + sealed class SignInError : Throwable() { class NotExistUsername() : SignInError() class PasswordNotMatchingWithUsername() : SignInError() @@ -10,6 +18,9 @@ sealed class SignInError : Throwable() { sealed class SignUpError : Throwable() { class InvalidUsername() : SignUpError() class InvalidPassword() : SignUpError() + class InvalidHobby() : SignUpError() class UsernameInputEmpty() : SignUpError() class PasswordInputEmpty() : SignUpError() + class HobbyInputEmpty() : SignUpError() + class AlreadyExistUsername() : SignUpError() } \ No newline at end of file From d787e18141da97208609f6a965462763e1126e7b Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:55:43 +0900 Subject: [PATCH 17/41] =?UTF-8?q?fix:=20runCatching=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/domain/usecase/SignUpAccountUseCase.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt index f41f1a6..26125c7 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignUpAccountUseCase.kt @@ -11,14 +11,14 @@ class SignUpAccountUseCase @Inject constructor( ) { suspend operator fun invoke(username: String, password: String, hobby: String): Result { - return runCatching { - when { - username.isBlank() -> Result.failure(SignUpError.UsernameInputEmpty()) - password.isBlank() -> Result.failure(SignUpError.PasswordInputEmpty()) - username.isValidUsername().not() -> Result.failure(SignUpError.InvalidUsername()) - password.isValidPassword().not() -> Result.failure(SignUpError.InvalidPassword()) - else -> userRepository.signUp(username, password, hobby) - } + return when { + username.isBlank() -> Result.failure(SignUpError.UsernameInputEmpty()) + password.isBlank() -> Result.failure(SignUpError.PasswordInputEmpty()) + hobby.isBlank() -> Result.failure(SignUpError.HobbyInputEmpty()) + username.isValidUsername().not() -> Result.failure(SignUpError.InvalidUsername()) + password.isValidPassword().not() -> Result.failure(SignUpError.InvalidPassword()) + hobby.isValidUsername().not() -> Result.failure(SignUpError.InvalidHobby()) + else -> userRepository.signUp(username, password, hobby) } } } \ No newline at end of file From 1279cf886dcfb09b9fa6775fa3a4f58609ba21fb Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:56:11 +0900 Subject: [PATCH 18/41] =?UTF-8?q?add:=20runCatching=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exception/ExceptionExtensions.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt diff --git a/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt b/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt new file mode 100644 index 0000000..f4cae5e --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt @@ -0,0 +1,32 @@ +package com.sopt.domain.exception + +import kotlin.coroutines.cancellation.CancellationException + +suspend fun T.runCatchingByCode( + vararg exceptions: Pair, + block: suspend T.() -> R +): Result { + return try { + Result.success(block()) + } catch (e: NetworkError) { + exceptions.find { + e.errorCode == it.first + }?.let { Result.failure(it.second) } ?: Result.failure(e) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} + +suspend fun T.runSuspendCatching( + block: suspend T.() -> R +): Result { + return try { + Result.success(block()) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} \ No newline at end of file From d099448248ed22310a34a66bfd34c8d855a4d660 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:56:41 +0900 Subject: [PATCH 19/41] =?UTF-8?q?refactor:=20runCatching=20->=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/sopt/data/dto/user/SignUpDto.kt | 2 +- .../com/sopt/data/repository/UserRepositoryImpl.kt | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt index 4a4f63b..01720d7 100644 --- a/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt +++ b/data/src/main/java/com/sopt/data/dto/user/SignUpDto.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class SignUpDto( - @SerialName("no") val no: Int? + @SerialName("no") val no: Int ) diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 94674e0..e843362 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -5,6 +5,9 @@ import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.domain.exception.SignUpError +import com.sopt.domain.exception.runCatchingByCode +import com.sopt.domain.exception.runSuspendCatching import com.sopt.domain.repository.UserRepository import javax.inject.Inject @@ -15,22 +18,22 @@ class UserRepositoryImpl @Inject constructor( ) : UserRepository { override suspend fun signUp(username: String, password: String, hobby: String): Result { - return runCatching { + return runCatchingByCode(0 to SignUpError.AlreadyExistUsername()) { userRemoteDataSource.signUp(SignUpRequest(username, password, hobby)) } } override suspend fun signIn(username: String, password: String): Result { - return runCatching { + return runSuspendCatching { val signInDto = userRemoteDataSource.signIn(SignInRequest(username, password)) signInDto.token?.let { tokenLocalDataSource.saveToken(it) } } } override suspend fun getMyHobby(): Result { - return runCatching { + return runSuspendCatching { val token = tokenLocalDataSource.getToken() userRemoteDataSource.getMyHobby(token).hobby ?: "" } } -} \ No newline at end of file +} From 834c5ab2f54195cf8524da45472ff906e490347d Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 00:56:58 +0900 Subject: [PATCH 20/41] =?UTF-8?q?add:=20=EC=B7=A8=EB=AF=B8=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/navigation/WavveNavigation.kt | 2 +- .../signup/composable/SignUpContentScreen.kt | 43 ++----------------- .../screen/signup/composable/SignUpScreen.kt | 25 ++++++++--- .../signup/viewmodel/SignUpViewModel.kt | 6 +++ presentation/src/main/res/values/strings.xml | 3 ++ 5 files changed, 34 insertions(+), 45 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt index 90d644a..904b930 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt @@ -70,7 +70,7 @@ fun WavveNavigation( NavHost( modifier = Modifier, navController = navController, - startDestination = Routes.Main.Graph, + startDestination = Routes.Auth.Graph, enterTransition = { slideInVertically { it } + fadeIn() }, exitTransition = { diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt index fd44696..c42e5a7 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpContentScreen.kt @@ -1,6 +1,5 @@ package com.sopt.presentation.ui.screen.signup.composable -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding @@ -8,22 +7,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import com.sopt.domain.util.isValidUsername import com.sopt.domain.util.isValidHobby import com.sopt.domain.util.isValidPassword +import com.sopt.domain.util.isValidUsername import com.sopt.presentation.R import com.sopt.presentation.ui.component.button.FullWidthTextButton @@ -39,21 +32,16 @@ fun SignUpContentScreen( onSignUpButtonClicked: () -> Unit = { } ) { - val context = LocalContext.current - val keyboardController = LocalSoftwareKeyboardController.current - val signUpButtonActivated by remember( usernameInput, - passwordInput + passwordInput, + hobbyInput ) { derivedStateOf { usernameInput.isValidUsername() && passwordInput.isValidPassword() && hobbyInput.isValidHobby() } } - var signUpFailureMessage by remember { mutableStateOf("") } - val toast = Toast.makeText(context, signUpFailureMessage, Toast.LENGTH_SHORT) - Column( modifier = Modifier .fillMaxSize() @@ -81,32 +69,9 @@ fun SignUpContentScreen( modifier = Modifier, text = stringResource(R.string.wavve_sign_up), activated = signUpButtonActivated, - onClick = { - if (signUpButtonActivated) { - keyboardController?.hide() - onSignUpButtonClicked() - } else { - when { - usernameInput.isValidUsername().not() -> toast.show() - passwordInput.isValidPassword().not() -> toast.show() - } - } - } + onClick = onSignUpButtonClicked ) } - - LaunchedEffect(signUpButtonActivated) { - when { - usernameInput.isValidUsername().not() -> { - signUpFailureMessage = ContextCompat.getString(context, R.string.check_email_format) - } - - passwordInput.isValidPassword().not() -> { - signUpFailureMessage = - ContextCompat.getString(context, R.string.check_password_format) - } - } - } } @Composable diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt index 3f55e3d..9987a8e 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -34,9 +35,9 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun SignUpScreen( + onSignUpSuccess: () -> Unit, modifier: Modifier = Modifier, onActionIconClicked: () -> Unit = {}, - onSignUpSuccess: () -> Unit, viewModel: SignUpViewModel = hiltViewModel(), ) { @@ -44,6 +45,7 @@ fun SignUpScreen( val passwordInput by viewModel.passwordInput.collectAsStateWithLifecycle() val hobbyInput by viewModel.hobbyInput.collectAsStateWithLifecycle() + val keyboardController = LocalSoftwareKeyboardController.current val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -69,7 +71,7 @@ fun SignUpScreen( ) }, snackbarHost = { SnackbarHost( - modifier = Modifier.imePadding(), + modifier = Modifier.imePadding().padding(bottom = 40.dp), hostState = snackbarHostState ) { TextSnackbar( @@ -99,18 +101,31 @@ fun SignUpScreen( viewModel.signUpUiState.collect { var snackbarMessage = "" when (it) { - is SignUpUiState.Success -> onSignUpSuccess() + is SignUpUiState.Success -> { + keyboardController?.hide() + onSignUpSuccess() + } + is SignUpUiState.UsernameInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_username_input) is SignUpUiState.PasswordInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_password_input) + is SignUpUiState.HobbyInputEmpty -> snackbarMessage = + ContextCompat.getString(context, R.string.require_hobby_input) + is SignUpUiState.InvalidUsername -> snackbarMessage = - ContextCompat.getString(context, R.string.not_exist_email) + ContextCompat.getString(context, R.string.check_email_format) is SignUpUiState.InvalidPassword -> snackbarMessage = - ContextCompat.getString(context, R.string.not_exist_password) + ContextCompat.getString(context, R.string.check_password_format) + + is SignUpUiState.InvalidHobby -> snackbarMessage = + ContextCompat.getString(context, R.string.check_hobby_format) + + is SignUpUiState.AlreadyExistUsername -> snackbarMessage = + ContextCompat.getString(context, R.string.already_exist_username) } if (it !is SignUpUiState.Success) scope.launch { diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt index 60f64ae..bf9d7fd 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/viewmodel/SignUpViewModel.kt @@ -51,8 +51,11 @@ class SignUpViewModel @Inject constructor( when (it) { is SignUpError.UsernameInputEmpty -> signUpUiState.emit(SignUpUiState.UsernameInputEmpty) is SignUpError.PasswordInputEmpty -> signUpUiState.emit(SignUpUiState.PasswordInputEmpty) + is SignUpError.HobbyInputEmpty -> signUpUiState.emit(SignUpUiState.HobbyInputEmpty) is SignUpError.InvalidUsername -> signUpUiState.emit(SignUpUiState.InvalidUsername) is SignUpError.InvalidPassword -> signUpUiState.emit(SignUpUiState.InvalidPassword) + is SignUpError.InvalidHobby -> signUpUiState.emit(SignUpUiState.InvalidHobby) + is SignUpError.AlreadyExistUsername -> signUpUiState.emit(SignUpUiState.AlreadyExistUsername) } } } @@ -63,6 +66,9 @@ sealed interface SignUpUiState { data object Success : SignUpUiState data object UsernameInputEmpty : SignUpUiState data object PasswordInputEmpty : SignUpUiState + data object HobbyInputEmpty : SignUpUiState data object InvalidUsername : SignUpUiState data object InvalidPassword : SignUpUiState + data object InvalidHobby : SignUpUiState + data object AlreadyExistUsername : SignUpUiState } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 25781d0..c81c08a 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -48,6 +48,8 @@ Wavve 회원가입 이메일 형식을 확인해주세요. 비밀번호 형식을 확인해주세요. + 취미 형식을 확인해주세요. + 이미 존재하는 아이디입니다." 비밀번호 @@ -60,6 +62,7 @@ 로그인 실패 아이디를 입력해주세요. 비밀번호를 입력해주세요. + 취미를 입력해주세요. 존재하지 않는 아이디입니다. 비밀번호가 일치하지 않습니다. From 392f586ae48d4c894477c9f360bb3efb61434f54 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:01:48 +0900 Subject: [PATCH 21/41] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/signup/composable/SignUpInputContentView.kt | 2 +- presentation/src/main/res/values/strings.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt index 57bce7a..cfd43a1 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signup/composable/SignUpInputContentView.kt @@ -124,7 +124,7 @@ fun SignUpInputContentView( IconFrontText( modifier = Modifier.padding(top = 12.dp), - text = stringResource(R.string.sign_up_caution_password, 8, 20), + text = stringResource(R.string.sign_up_caution_password, 1, 8), painter = painterResource(R.drawable.ic_caution), ) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c81c08a..c488910 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -46,10 +46,10 @@ 또는 다른 서비스 계정으로 가입 SNS계정으로 간편하게 가입하여 서비스를 이용하실 수 있습니다. 기존 POOQ 계정 또는 Wavve 계정과는 연동되지 않으니 이용에 참고하세요. Wavve 회원가입 - 이메일 형식을 확인해주세요. - 비밀번호 형식을 확인해주세요. - 취미 형식을 확인해주세요. - 이미 존재하는 아이디입니다." + 아이디를 1글자 이상, 8글자 이내로 입력해주세요. + 비밀번호를 1글자 이상, 8글자 이내로 입력해주세요. + 취미를 1글자 이상, 8글자 이내로 입력해주세요. + 이미 존재하는 아이디입니다. 비밀번호 From 19f90f1d8df3460eb6983de4823a26e69634a3c6 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:03:07 +0900 Subject: [PATCH 22/41] =?UTF-8?q?fix:=20runCatching=20=EC=A4=91=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sopt/domain/usecase/SignInUseCase.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt index 9d8fa17..5cf72d9 100644 --- a/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt +++ b/domain/src/main/java/com/sopt/domain/usecase/SignInUseCase.kt @@ -9,12 +9,10 @@ class SignInUseCase @Inject constructor( ) { suspend operator fun invoke(username: String, password: String): Result { - return runCatching { - when { - username.isBlank() -> Result.failure(SignInError.UsernameInputEmpty()) - password.isBlank() -> Result.failure(SignInError.PasswordInputEmpty()) - else -> userRepository.signIn(username, password) - } + return when { + username.isBlank() -> Result.failure(SignInError.UsernameInputEmpty()) + password.isBlank() -> Result.failure(SignInError.PasswordInputEmpty()) + else -> userRepository.signIn(username, password) } } } \ No newline at end of file From 0c1c9a6045aad8688091525385cada8d488c69af Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:14:23 +0900 Subject: [PATCH 23/41] =?UTF-8?q?add:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/data/repository/UserRepositoryImpl.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index e843362..3df5247 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.domain.exception.SignInError import com.sopt.domain.exception.SignUpError import com.sopt.domain.exception.runCatchingByCode import com.sopt.domain.exception.runSuspendCatching @@ -24,7 +25,10 @@ class UserRepositoryImpl @Inject constructor( } override suspend fun signIn(username: String, password: String): Result { - return runSuspendCatching { + return runCatchingByCode( + 2 to SignInError.NotExistUsername(), + 1 to SignInError.PasswordNotMatchingWithUsername() + ) { val signInDto = userRemoteDataSource.signIn(SignInRequest(username, password)) signInDto.token?.let { tokenLocalDataSource.saveToken(it) } } From 05f69b153c37dac95f1d413c6709dfec91ed124f Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:14:32 +0900 Subject: [PATCH 24/41] =?UTF-8?q?add:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/signin/composable/SignInScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt index 095186f..6216ccf 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/composable/SignInScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -42,6 +43,7 @@ fun SignInScreen( ) { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + val keyboardController = LocalSoftwareKeyboardController.current val context = LocalContext.current val usernameInput by viewModel.usernameInput.collectAsStateWithLifecycle() @@ -95,7 +97,10 @@ fun SignInScreen( viewModel.signInUiState.collect { var snackbarMessage = "" when (it) { - is SignInUiState.Success -> onSignInSuccess() + is SignInUiState.Success -> { + onSignInSuccess() + keyboardController?.hide() + } is SignInUiState.UsernameInputEmpty -> snackbarMessage = ContextCompat.getString(context, R.string.require_username_input) From 743bb86f7b92682a0e3b19646fd9c597babd83f0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:19:31 +0900 Subject: [PATCH 25/41] chore: get -> fetch --- data/src/main/java/com/sopt/data/api/remote/UserApi.kt | 2 +- .../com/sopt/data/datasource/remote/UserRemoteDataSource.kt | 4 ++-- .../main/java/com/sopt/data/repository/UserRepositoryImpl.kt | 4 ++-- .../main/java/com/sopt/domain/repository/UserRepository.kt | 2 +- .../sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index 3a8930c..49bd91f 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -19,5 +19,5 @@ interface UserApi { suspend fun signIn(@Body signInRequest: SignInRequest): SignInDto @GET("user/my-hobby") - suspend fun getMyHobby(@Header("token") token: String): HobbyDto + suspend fun fetchMyHobby(@Header("token") token: String): HobbyDto } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index 2ffa5b1..60415a1 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -20,7 +20,7 @@ class UserRemoteDataSource @Inject constructor( return userApi.signIn(signInRequest) } - suspend fun getMyHobby(token: String): HobbyDto { - return userApi.getMyHobby(token) + suspend fun fetchMyHobby(token: String): HobbyDto { + return userApi.fetchMyHobby(token) } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 3df5247..0669f43 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -34,10 +34,10 @@ class UserRepositoryImpl @Inject constructor( } } - override suspend fun getMyHobby(): Result { + override suspend fun fetchMyHobby(): Result { return runSuspendCatching { val token = tokenLocalDataSource.getToken() - userRemoteDataSource.getMyHobby(token).hobby ?: "" + userRemoteDataSource.fetchMyHobby(token).hobby ?: "" } } } diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index adfbd9b..0cdbc7c 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -3,5 +3,5 @@ package com.sopt.domain.repository interface UserRepository { suspend fun signUp(username: String, password: String, hobby: String): Result suspend fun signIn(username: String, password: String): Result - suspend fun getMyHobby(): Result + suspend fun fetchMyHobby(): Result } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt index 806d171..fd515ce 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt @@ -16,7 +16,7 @@ class MyViewModel @Inject constructor( ) : ViewModel() { val myHobby = flow { - emit(userRepository.getMyHobby()) + emit(userRepository.fetchMyHobby()) }.transform { result -> result.onSuccess { emit(it) From c37bf31f87cfab946ae01474eff07bcc1d082dd5 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:22:54 +0900 Subject: [PATCH 26/41] =?UTF-8?q?add:=20=EB=8B=A4=EB=A5=B8=EC=82=AC?= =?UTF-8?q?=EB=9E=8C=20=EC=B7=A8=EB=AF=B8=20=EC=A1=B0=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/main/java/com/sopt/data/api/remote/UserApi.kt | 4 ++++ .../sopt/data/datasource/remote/UserRemoteDataSource.kt | 4 ++++ .../java/com/sopt/data/repository/UserRepositoryImpl.kt | 8 ++++++++ .../main/java/com/sopt/domain/exception/NetworkError.kt | 4 ++++ .../java/com/sopt/domain/repository/UserRepository.kt | 1 + 5 files changed, 21 insertions(+) diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index 49bd91f..b3aebc9 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -9,6 +9,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Path interface UserApi { @@ -20,4 +21,7 @@ interface UserApi { @GET("user/my-hobby") suspend fun fetchMyHobby(@Header("token") token: String): HobbyDto + + @GET("user/{no}/hobby") + suspend fun fetchUserHobby(@Header("token") token: String, @Path("no") no: Int): HobbyDto } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index 60415a1..a45ee1b 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -23,4 +23,8 @@ class UserRemoteDataSource @Inject constructor( suspend fun fetchMyHobby(token: String): HobbyDto { return userApi.fetchMyHobby(token) } + + suspend fun fetchUserHobby(token: String, no: Int): HobbyDto { + return userApi.fetchUserHobby(token, no) + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 0669f43..9157383 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.domain.exception.HobbyError import com.sopt.domain.exception.SignInError import com.sopt.domain.exception.SignUpError import com.sopt.domain.exception.runCatchingByCode @@ -40,4 +41,11 @@ class UserRepositoryImpl @Inject constructor( userRemoteDataSource.fetchMyHobby(token).hobby ?: "" } } + + override suspend fun fetchUserHobby(no: Int): Result { + return runCatchingByCode(1 to HobbyError.NotExistUserNo()) { + val token = tokenLocalDataSource.getToken() + userRemoteDataSource.fetchUserHobby(token, no).hobby ?: "" + } + } } diff --git a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt index 473089a..d613c49 100644 --- a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt +++ b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt @@ -23,4 +23,8 @@ sealed class SignUpError : Throwable() { class PasswordInputEmpty() : SignUpError() class HobbyInputEmpty() : SignUpError() class AlreadyExistUsername() : SignUpError() +} + +sealed class HobbyError : Throwable() { + class NotExistUserNo : HobbyError() } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index 0cdbc7c..a28a7dc 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -4,4 +4,5 @@ interface UserRepository { suspend fun signUp(username: String, password: String, hobby: String): Result suspend fun signIn(username: String, password: String): Result suspend fun fetchMyHobby(): Result + suspend fun fetchUserHobby(no: Int): Result } \ No newline at end of file From c25a6800c3292a9131784b6e0543eaec036720e0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 01:54:24 +0900 Subject: [PATCH 27/41] =?UTF-8?q?add:=20=EB=8B=A4=EB=A5=B8=EC=82=AC?= =?UTF-8?q?=EB=9E=8C=20=EC=B7=A8=EB=AF=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B0=8F=20=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/UserRepositoryImpl.kt | 4 +- .../com/sopt/domain/exception/NetworkError.kt | 6 ++- .../sopt/domain/usecase/SearchHobbyUseCase.kt | 18 +++++++ .../screen/search/composable/SearchScreen.kt | 48 +++++++++++++++++-- .../search/viewmodel/SearchViewModel.kt | 34 ++++++++++++- presentation/src/main/res/values/strings.xml | 3 ++ 6 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 domain/src/main/java/com/sopt/domain/usecase/SearchHobbyUseCase.kt diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index 9157383..a22795e 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -5,7 +5,7 @@ import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest -import com.sopt.domain.exception.HobbyError +import com.sopt.domain.exception.SearchHobbyError import com.sopt.domain.exception.SignInError import com.sopt.domain.exception.SignUpError import com.sopt.domain.exception.runCatchingByCode @@ -43,7 +43,7 @@ class UserRepositoryImpl @Inject constructor( } override suspend fun fetchUserHobby(no: Int): Result { - return runCatchingByCode(1 to HobbyError.NotExistUserNo()) { + return runCatchingByCode(1 to SearchHobbyError.NotExistUserNo()) { val token = tokenLocalDataSource.getToken() userRemoteDataSource.fetchUserHobby(token, no).hobby ?: "" } diff --git a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt index d613c49..6b42a47 100644 --- a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt +++ b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt @@ -25,6 +25,8 @@ sealed class SignUpError : Throwable() { class AlreadyExistUsername() : SignUpError() } -sealed class HobbyError : Throwable() { - class NotExistUserNo : HobbyError() +sealed class SearchHobbyError : Throwable() { + class InputEmpty : SearchHobbyError() + class InputNotNumber : SearchHobbyError() + class NotExistUserNo : SearchHobbyError() } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/usecase/SearchHobbyUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/SearchHobbyUseCase.kt new file mode 100644 index 0000000..3097466 --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/usecase/SearchHobbyUseCase.kt @@ -0,0 +1,18 @@ +package com.sopt.domain.usecase + +import com.sopt.domain.exception.SearchHobbyError +import com.sopt.domain.repository.UserRepository +import javax.inject.Inject + +class SearchHobbyUseCase @Inject constructor( + private val userRepository: UserRepository +) { + + suspend operator fun invoke(query: String): Result { + return when { + query.isBlank() -> Result.failure(SearchHobbyError.InputEmpty()) + query.toIntOrNull() == null -> Result.failure(SearchHobbyError.InputNotNumber()) + else -> userRepository.fetchUserHobby(query.toInt()) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt index d0e8214..47ced17 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt @@ -1,16 +1,24 @@ package com.sopt.presentation.ui.screen.search.composable +import android.widget.Toast +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -20,9 +28,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.presentation.R import com.sopt.presentation.ui.component.surface.DefaultSurface import com.sopt.presentation.ui.component.textfield.BottomLinedTextField +import com.sopt.presentation.ui.screen.search.viewmodel.SearchResultUiState import com.sopt.presentation.ui.screen.search.viewmodel.SearchViewModel import com.sopt.presentation.ui.theme.WavveTheme import com.sopt.presentation.ui.util.noRippleClickable +import kotlinx.coroutines.flow.collect @Composable fun SearchScreen( @@ -30,7 +40,10 @@ fun SearchScreen( viewModel: SearchViewModel = hiltViewModel() ) { - val searchQuery = viewModel.searchQuery.collectAsStateWithLifecycle() + val context = LocalContext.current + + val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() + val searchedHobby by viewModel.searchedHobby.collectAsStateWithLifecycle(SearchResultUiState.Nothing) DefaultSurface( modifier = modifier @@ -42,12 +55,14 @@ fun SearchScreen( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), - value = searchQuery.value, + value = searchQuery, color = WavveTheme.colorScheme.background, onValueChange = viewModel::onSearchQueryChanged, placeholder = stringResource(R.string.search_placeholder), keyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Search + ), keyboardActions = KeyboardActions( + onSearch = { viewModel.search(searchQuery) } ), leadingIcon = { Icon( @@ -56,7 +71,7 @@ fun SearchScreen( tint = WavveTheme.colorScheme.tertiary ) }, trailingIcon = { - if (searchQuery.value.isNotEmpty()) { + if (searchQuery.isNotEmpty()) { Icon( modifier = Modifier .size(16.dp) @@ -70,6 +85,20 @@ fun SearchScreen( } } ) + + when { + searchedHobby is SearchResultUiState.Success -> { + val hobby = (searchedHobby as SearchResultUiState.Success).hobby + Box( + modifier = Modifier.fillMaxWidth().padding(32.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "너의 취미는 $hobby", + ) + } + } + } SearchContentScreen( modifier = Modifier .fillMaxSize() @@ -78,4 +107,17 @@ fun SearchScreen( ) } } + + LaunchedEffect(Unit) { + viewModel.searchedHobby.collect { + when { + it is SearchResultUiState.InputEmpty -> + Toast.makeText(context, context.getString(R.string.require_search_input), Toast.LENGTH_SHORT).show() + it is SearchResultUiState.InputNotNumber -> + Toast.makeText(context, context.getString(R.string.require_only_number), Toast.LENGTH_SHORT).show() + it is SearchResultUiState.NotExistUser -> + Toast.makeText(context, context.getString(R.string.not_exist_search_result), Toast.LENGTH_SHORT).show() + } + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt index 78f1fd5..0d8e9ee 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt @@ -2,13 +2,21 @@ package com.sopt.presentation.ui.screen.search.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sopt.domain.exception.SearchHobbyError +import com.sopt.domain.usecase.SearchHobbyUseCase import com.sopt.presentation.ui.state.VideoOverviewViewState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle + val savedStateHandle: SavedStateHandle, + private val searchHobbyUseCase: SearchHobbyUseCase ): ViewModel() { val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") @@ -73,12 +81,36 @@ class SearchViewModel @Inject constructor( ) ) + val searchedHobby: SharedFlow + field = MutableSharedFlow() fun onSearchQueryChanged(query: String) { savedStateHandle[SEARCH_QUERY] = query } + + fun search(query: String) { + viewModelScope.launch { + searchHobbyUseCase(query).onSuccess { + searchedHobby.emit(SearchResultUiState.Success(it)) + }.onFailure { + when (it) { + is SearchHobbyError.InputEmpty -> searchedHobby.emit(SearchResultUiState.InputEmpty) + is SearchHobbyError.InputNotNumber -> searchedHobby.emit(SearchResultUiState.InputNotNumber) + is SearchHobbyError.NotExistUserNo -> searchedHobby.emit(SearchResultUiState.NotExistUser) + } + } + } + } companion object { const val SEARCH_QUERY = "searchQuery" } +} + +sealed interface SearchResultUiState { + data object Nothing: SearchResultUiState + data class Success(val hobby: String): SearchResultUiState + data object InputEmpty: SearchResultUiState + data object InputNotNumber: SearchResultUiState + data object NotExistUser: SearchResultUiState } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index c488910..47802f5 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -78,5 +78,8 @@ 제목, 장르, 배우로 찾아보세요 + 검색어를 입력해주세요. + 숫자만 입력해주세요. + 존재하지 않는 사용자입니다. \ No newline at end of file From 0aff239d3db2b0dcb7cd180f7597be507ab79884 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 02:00:58 +0900 Subject: [PATCH 28/41] =?UTF-8?q?add:=20=EB=82=B4=20=EC=84=A4=EC=A0=95=20r?= =?UTF-8?q?oute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sopt/presentation/ui/navigation/Routes.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt index 503aa11..41118aa 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt @@ -22,4 +22,9 @@ object Routes { @Serializable object Graph @Serializable data class Video(val videoOverviewViewState: VideoOverviewViewState) } + + object MySetting { + @Serializable object Graph + @Serializable object EditProfile + } } \ No newline at end of file From dc2a5e507b6c25134b4900d942dc6593a958c0f8 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 02:02:05 +0900 Subject: [PATCH 29/41] =?UTF-8?q?add:=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/presentation/ui/navigation/WavveNavigation.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt index 904b930..1478bb7 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt @@ -133,6 +133,14 @@ fun WavveNavigation( } } + navigation( + startDestination = Routes.MySetting.EditProfile + ) { + composable { + + } + } + navigation( startDestination = Routes.VideoDetail.Video(VideoOverviewViewState.Empty) ) { From 955025598b97dbd685e99465a75ee8f78b6ed7fb Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 03:58:42 +0900 Subject: [PATCH 30/41] add: domain coroutine dependency --- domain/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 777d71a..2f1b6a3 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -10,4 +10,5 @@ java { dependencies { implementation(libs.javax.inject) + implementation(libs.kotlinx.coroutines.core) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea76cc5..5cf7fc7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" +kotlinxCoroutinesCore = "1.9.0" kotlinxSerializationJson = "1.7.3" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.2" @@ -41,6 +42,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } From a583091297d5e747f5f41533beee8c101475b473 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 03:59:19 +0900 Subject: [PATCH 31/41] =?UTF-8?q?add:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/main/java/com/sopt/data/api/remote/UserApi.kt | 8 ++++++++ .../sopt/data/datasource/remote/UserRemoteDataSource.kt | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt index b3aebc9..4e1286a 100644 --- a/data/src/main/java/com/sopt/data/api/remote/UserApi.kt +++ b/data/src/main/java/com/sopt/data/api/remote/UserApi.kt @@ -5,10 +5,12 @@ import com.sopt.data.dto.user.SignInDto import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.data.request.UpdateProfileRequest import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path interface UserApi { @@ -24,4 +26,10 @@ interface UserApi { @GET("user/{no}/hobby") suspend fun fetchUserHobby(@Header("token") token: String, @Path("no") no: Int): HobbyDto + + @PUT("user") + suspend fun updateProfile( + @Header("token") token: String, + @Body updateProfileRequest: UpdateProfileRequest + ) } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt index a45ee1b..1ef9ad6 100644 --- a/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/remote/UserRemoteDataSource.kt @@ -6,6 +6,7 @@ import com.sopt.data.dto.user.SignInDto import com.sopt.data.dto.user.SignUpDto import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.data.request.UpdateProfileRequest import javax.inject.Inject class UserRemoteDataSource @Inject constructor( @@ -27,4 +28,8 @@ class UserRemoteDataSource @Inject constructor( suspend fun fetchUserHobby(token: String, no: Int): HobbyDto { return userApi.fetchUserHobby(token, no) } + + suspend fun updateProfile(token: String, updateProfileRequest: UpdateProfileRequest) { + return userApi.updateProfile(token, updateProfileRequest) + } } \ No newline at end of file From 243cc6054650644d6fb683362b21794e91d925c9 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 03:59:54 +0900 Subject: [PATCH 32/41] =?UTF-8?q?add:=20=EB=94=94=EC=8A=A4=ED=8C=A8?= =?UTF-8?q?=EC=B2=98=20DI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/and/di/CoroutineModule.kt | 19 +++++++++++++++++++ .../src/main/java/com/sopt/data/Qualifiers.kt | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/sopt/and/di/CoroutineModule.kt diff --git a/app/src/main/java/org/sopt/and/di/CoroutineModule.kt b/app/src/main/java/org/sopt/and/di/CoroutineModule.kt new file mode 100644 index 0000000..6c1ef53 --- /dev/null +++ b/app/src/main/java/org/sopt/and/di/CoroutineModule.kt @@ -0,0 +1,19 @@ +package org.sopt.and.di + +import com.sopt.data.IODispatcher +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CoroutineModule { + + @IODispatcher + @Singleton + @Provides + fun provideIODispatcher() = Dispatchers.IO +} \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/Qualifiers.kt b/data/src/main/java/com/sopt/data/Qualifiers.kt index d8b3b59..0fd2180 100644 --- a/data/src/main/java/com/sopt/data/Qualifiers.kt +++ b/data/src/main/java/com/sopt/data/Qualifiers.kt @@ -8,4 +8,8 @@ annotation class UserSharedPref @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class TokenEncryptedSharedPref \ No newline at end of file +annotation class TokenEncryptedSharedPref + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class IODispatcher \ No newline at end of file From f2d7cbd40ebbff4b2bc39cd5573d8fb9d4242e40 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 04:01:13 +0900 Subject: [PATCH 33/41] add: EditProfileScreen --- .../ui/navigation/WavveNavigation.kt | 13 ++- .../screen/my/composable/EditProfileScreen.kt | 97 +++++++++++++++++++ .../screen/my/composable/MyContentScreen.kt | 12 ++- .../ui/screen/my/composable/MyScreen.kt | 4 +- presentation/src/main/res/values/strings.xml | 7 ++ 5 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/EditProfileScreen.kt diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt index 1478bb7..f2dce06 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt @@ -32,6 +32,7 @@ import com.sopt.presentation.ui.component.bottom.WavveBottomBar import com.sopt.presentation.ui.component.bottom.WavveBottomBarItem import com.sopt.presentation.ui.navigation.navtype.VideoOverviewNavType import com.sopt.presentation.ui.screen.home.composable.HomeScreen +import com.sopt.presentation.ui.screen.my.composable.EditProfileScreen import com.sopt.presentation.ui.screen.my.composable.MyScreen import com.sopt.presentation.ui.screen.search.composable.SearchScreen import com.sopt.presentation.ui.screen.signin.composable.SignInScreen @@ -128,7 +129,10 @@ fun WavveNavigation( MyScreen( modifier = Modifier .padding(innerPadding) - .fillMaxSize() + .fillMaxSize(), + onNavigateToEditProfile = { + navController.navigate(Routes.MySetting.EditProfile) + } ) } } @@ -137,7 +141,12 @@ fun WavveNavigation( startDestination = Routes.MySetting.EditProfile ) { composable { - + EditProfileScreen( + modifier = Modifier.fillMaxSize(), + onCompleteEdit = { + navController.popBackStack() + } + ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/EditProfileScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/EditProfileScreen.kt new file mode 100644 index 0000000..059c039 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/EditProfileScreen.kt @@ -0,0 +1,97 @@ +package com.sopt.presentation.ui.screen.my.composable + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sopt.presentation.R +import com.sopt.presentation.ui.component.button.FullWidthTextButton +import com.sopt.presentation.ui.component.textfield.FilledTextField +import com.sopt.presentation.ui.component.top.DefaultCenterAlignedTopAppBar +import com.sopt.presentation.ui.screen.my.viewmodel.EditProfileViewModel +import com.sopt.presentation.ui.screen.my.viewmodel.UpdateProfileUiState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditProfileScreen( + modifier: Modifier = Modifier, + onCompleteEdit: () -> Unit, + viewModel: EditProfileViewModel = hiltViewModel() +) { + + val context = LocalContext.current + + val passwordInput by viewModel.passwordInput.collectAsStateWithLifecycle() + val hobbyInput by viewModel.hobbyInput.collectAsStateWithLifecycle() + + Column( + modifier = modifier + ) { + DefaultCenterAlignedTopAppBar( + title = { + Text(stringResource(R.string.title_profile_edit)) + } + ) + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + .weight(1f) + .verticalScroll(rememberScrollState()) + .imePadding() + ) { + FilledTextField( + value = passwordInput, + onValueChange = viewModel::onPasswordInputChanged, + placeholder = stringResource(R.string.edit_password_placeholder), + modifier = Modifier.padding(top = 16.dp) + ) + + FilledTextField( + value = hobbyInput, + onValueChange = viewModel::onHobbyInputChanged, + placeholder = stringResource(R.string.edit_hobby_placeholder), + modifier = Modifier.padding(top = 32.dp) + ) + } + + FullWidthTextButton( + text = stringResource(R.string.save), + onClick = viewModel::updateProfile, + ) + } + + LaunchedEffect(Unit) { + viewModel.updateProfileUiState.collect { + when(it) { + is UpdateProfileUiState.Success -> { + onCompleteEdit() + } + is UpdateProfileUiState.PasswordInputEmpty -> { + Toast.makeText(context, context.getString(R.string.require_password_input), Toast.LENGTH_SHORT).show() + } + is UpdateProfileUiState.HobbyInputEmpty -> { + Toast.makeText(context, context.getString(R.string.require_hobby_input), Toast.LENGTH_SHORT).show() + } + is UpdateProfileUiState.InvalidPassword -> { + Toast.makeText(context, context.getString(R.string.check_password_format), Toast.LENGTH_SHORT).show() + } + is UpdateProfileUiState.InvalidHobby -> { + Toast.makeText(context, context.getString(R.string.check_hobby_format), Toast.LENGTH_SHORT).show() + } + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt index cf32973..53fb3a7 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyContentScreen.kt @@ -25,10 +25,12 @@ import com.sopt.presentation.ui.component.surface.VariantSurface import com.sopt.presentation.ui.component.text.PrimaryText import com.sopt.presentation.ui.component.text.SecondaryText import com.sopt.presentation.ui.theme.WavveTheme +import com.sopt.presentation.ui.util.noRippleClickable @Composable fun MyContentScreen( myHobby: String, + onNavigateToEditProfile: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -59,7 +61,9 @@ fun MyContentScreen( contentDescription = stringResource(R.string.notification_content_description) ) PrimaryIcon( - modifier = Modifier.padding(start = 12.dp), + modifier = Modifier.padding(start = 12.dp).noRippleClickable { + onNavigateToEditProfile() + }, imageVector = Icons.Outlined.Settings, contentDescription = stringResource(R.string.notification_content_description) ) @@ -81,9 +85,8 @@ fun MyContentScreen( PrimaryIcon( modifier = Modifier.padding(start = 4.dp), imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = stringResource(R.string.profile_image), - - ) + contentDescription = stringResource(R.string.profile_image) + ) } } } @@ -154,5 +157,6 @@ fun MyContentScreen( private fun MyContentScreenPreview() { MyContentScreen( myHobby = "축구", + onNavigateToEditProfile = {} ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt index 17998e4..965e11b 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/composable/MyScreen.kt @@ -14,6 +14,7 @@ import com.sopt.presentation.ui.screen.my.viewmodel.MyViewModel @Composable fun MyScreen( modifier: Modifier = Modifier, + onNavigateToEditProfile: () -> Unit, viewModel: MyViewModel = hiltViewModel() ) { @@ -26,7 +27,8 @@ fun MyScreen( MyContentScreen( modifier = Modifier .fillMaxSize(), - myHobby = myHobby + myHobby = myHobby, + onNavigateToEditProfile = onNavigateToEditProfile ) } } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 47802f5..eb47221 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -82,4 +82,11 @@ 숫자만 입력해주세요. 존재하지 않는 사용자입니다. + + 프로필 수정 + 새 비밀번호를 입력해주세요. + 새 취미를 입력해주세요. + 저장 + + \ No newline at end of file From ffd39f2034d5351bb834c46d751102ab38632abe Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 04:01:47 +0900 Subject: [PATCH 34/41] =?UTF-8?q?refactor:=20=EC=B7=A8=EB=AF=B8=20fetching?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/UserRepositoryImpl.kt | 43 ++++++++-- .../sopt/data/request/UpdateProfileRequest.kt | 9 +++ .../com/sopt/domain/exception/NetworkError.kt | 7 ++ .../sopt/domain/repository/UserRepository.kt | 5 +- .../domain/usecase/UpdateProfileUseCase.kt | 22 ++++++ .../my/viewmodel/EditProfileViewModel.kt | 79 +++++++++++++++++++ .../ui/screen/my/viewmodel/MyViewModel.kt | 12 +-- 7 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 data/src/main/java/com/sopt/data/request/UpdateProfileRequest.kt create mode 100644 domain/src/main/java/com/sopt/domain/usecase/UpdateProfileUseCase.kt create mode 100644 presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index a22795e..dcef000 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -1,24 +1,47 @@ package com.sopt.data.repository +import com.sopt.data.IODispatcher import com.sopt.data.datasource.local.TokenLocalDataSource import com.sopt.data.datasource.local.UserLocalDataSource import com.sopt.data.datasource.remote.UserRemoteDataSource import com.sopt.data.request.SignInRequest import com.sopt.data.request.SignUpRequest +import com.sopt.data.request.UpdateProfileRequest import com.sopt.domain.exception.SearchHobbyError import com.sopt.domain.exception.SignInError import com.sopt.domain.exception.SignUpError import com.sopt.domain.exception.runCatchingByCode -import com.sopt.domain.exception.runSuspendCatching import com.sopt.domain.repository.UserRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn import javax.inject.Inject class UserRepositoryImpl @Inject constructor( private val userLocalDataSource: UserLocalDataSource, private val userRemoteDataSource: UserRemoteDataSource, - private val tokenLocalDataSource: TokenLocalDataSource + private val tokenLocalDataSource: TokenLocalDataSource, + @IODispatcher private val ioDispatcher: CoroutineDispatcher ) : UserRepository { + private val _myHobby = MutableStateFlow("") + private val myHobby: StateFlow = flow { + val token = tokenLocalDataSource.getToken() + _myHobby.value = userRemoteDataSource.fetchMyHobby(token).hobby ?: "" + + emitAll(_myHobby) + }.stateIn( + scope = CoroutineScope(ioDispatcher), + started = SharingStarted.Lazily, + initialValue = "" + ) + override suspend fun signUp(username: String, password: String, hobby: String): Result { return runCatchingByCode(0 to SignUpError.AlreadyExistUsername()) { userRemoteDataSource.signUp(SignUpRequest(username, password, hobby)) @@ -35,11 +58,8 @@ class UserRepositoryImpl @Inject constructor( } } - override suspend fun fetchMyHobby(): Result { - return runSuspendCatching { - val token = tokenLocalDataSource.getToken() - userRemoteDataSource.fetchMyHobby(token).hobby ?: "" - } + override fun fetchMyHobby(): Flow { + return myHobby } override suspend fun fetchUserHobby(no: Int): Result { @@ -48,4 +68,13 @@ class UserRepositoryImpl @Inject constructor( userRemoteDataSource.fetchUserHobby(token, no).hobby ?: "" } } + + override suspend fun updateProfile(password: String, hobby: String): Result { + return runCatchingByCode { + val token = tokenLocalDataSource.getToken() + userRemoteDataSource.updateProfile(token, UpdateProfileRequest(password, hobby)).also { + _myHobby.value = hobby + } + } + } } diff --git a/data/src/main/java/com/sopt/data/request/UpdateProfileRequest.kt b/data/src/main/java/com/sopt/data/request/UpdateProfileRequest.kt new file mode 100644 index 0000000..a87152b --- /dev/null +++ b/data/src/main/java/com/sopt/data/request/UpdateProfileRequest.kt @@ -0,0 +1,9 @@ +package com.sopt.data.request + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateProfileRequest( + val password: String, + val hobby: String +) diff --git a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt index 6b42a47..6cbfc72 100644 --- a/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt +++ b/domain/src/main/java/com/sopt/domain/exception/NetworkError.kt @@ -29,4 +29,11 @@ sealed class SearchHobbyError : Throwable() { class InputEmpty : SearchHobbyError() class InputNotNumber : SearchHobbyError() class NotExistUserNo : SearchHobbyError() +} + +sealed class UpdateProfileError : Throwable() { + class InvalidPassword() : UpdateProfileError() + class InvalidHobby() : UpdateProfileError() + class PasswordInputEmpty() : UpdateProfileError() + class HobbyInputEmpty() : UpdateProfileError() } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index a28a7dc..58bdb85 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -1,8 +1,11 @@ package com.sopt.domain.repository +import kotlinx.coroutines.flow.Flow + interface UserRepository { suspend fun signUp(username: String, password: String, hobby: String): Result suspend fun signIn(username: String, password: String): Result - suspend fun fetchMyHobby(): Result + fun fetchMyHobby(): Flow suspend fun fetchUserHobby(no: Int): Result + suspend fun updateProfile(password: String, hobby: String): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/usecase/UpdateProfileUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/UpdateProfileUseCase.kt new file mode 100644 index 0000000..138eebf --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/usecase/UpdateProfileUseCase.kt @@ -0,0 +1,22 @@ +package com.sopt.domain.usecase + +import com.sopt.domain.exception.UpdateProfileError +import com.sopt.domain.repository.UserRepository +import com.sopt.domain.util.isValidHobby +import com.sopt.domain.util.isValidPassword +import javax.inject.Inject + +class UpdateProfileUseCase @Inject constructor( + private val userRepository: UserRepository +) { + + suspend operator fun invoke(password: String, hobby: String): Result { + return when { + password.isBlank() -> Result.failure(UpdateProfileError.PasswordInputEmpty()) + hobby.isBlank() -> Result.failure(UpdateProfileError.HobbyInputEmpty()) + password.isValidPassword().not() -> Result.failure(UpdateProfileError.InvalidPassword()) + hobby.isValidHobby().not() -> Result.failure(UpdateProfileError.InvalidHobby()) + else -> userRepository.updateProfile(password, hobby) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt new file mode 100644 index 0000000..d0798d1 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt @@ -0,0 +1,79 @@ +package com.sopt.presentation.ui.screen.my.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sopt.domain.exception.UpdateProfileError +import com.sopt.domain.repository.UserRepository +import com.sopt.domain.usecase.UpdateProfileUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class EditProfileViewModel @Inject constructor( + private val userRepository: UserRepository, + private val updateProfileUseCase: UpdateProfileUseCase +) : ViewModel() { + + private val _passwordInput = MutableStateFlow("") + val passwordInput: StateFlow = _passwordInput.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = "" + ) + + private val _hobbyInput = MutableStateFlow("") + val hobbyInput = flow { + _hobbyInput.emit(userRepository.fetchMyHobby().first()) + emitAll(_hobbyInput) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = "" + ) + + val updateProfileUiState: SharedFlow + field = MutableSharedFlow() + + fun onPasswordInputChanged(password: String) { + _passwordInput.value = password + } + + fun onHobbyInputChanged(hobby: String) { + _hobbyInput.value = hobby + } + + fun updateProfile() { + viewModelScope.launch { + updateProfileUseCase(passwordInput.value, hobbyInput.value).onSuccess { + println("dddddddd $it") + updateProfileUiState.emit(UpdateProfileUiState.Success) + }.onFailure { + println("dddddddd2 $it") + when(it) { + is UpdateProfileError.PasswordInputEmpty -> updateProfileUiState.emit(UpdateProfileUiState.PasswordInputEmpty) + is UpdateProfileError.HobbyInputEmpty -> updateProfileUiState.emit(UpdateProfileUiState.HobbyInputEmpty) + is UpdateProfileError.InvalidPassword -> updateProfileUiState.emit(UpdateProfileUiState.InvalidPassword) + is UpdateProfileError.InvalidHobby -> updateProfileUiState.emit(UpdateProfileUiState.InvalidHobby) + } + } + } + } +} + +sealed interface UpdateProfileUiState { + data object Success : UpdateProfileUiState + data object PasswordInputEmpty : UpdateProfileUiState + data object HobbyInputEmpty : UpdateProfileUiState + data object InvalidPassword : UpdateProfileUiState + data object InvalidHobby : UpdateProfileUiState +} diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt index fd515ce..a159a39 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/MyViewModel.kt @@ -5,7 +5,9 @@ import androidx.lifecycle.viewModelScope import com.sopt.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transform import javax.inject.Inject @@ -15,14 +17,8 @@ class MyViewModel @Inject constructor( private val userRepository: UserRepository ) : ViewModel() { - val myHobby = flow { - emit(userRepository.fetchMyHobby()) - }.transform { result -> - result.onSuccess { - emit(it) - }.onFailure { - emit("Unknown") - } + val myHobby = userRepository.fetchMyHobby().catch { + emit("Error") }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), From c6f0a26ea30d98dee19330110d8ed6897c095409 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 04:02:09 +0900 Subject: [PATCH 35/41] =?UTF-8?q?fix:=20navigation=20=EC=95=A0=EB=8B=88?= =?UTF-8?q?=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0(=EC=9E=84?= =?UTF-8?q?=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/navigation/WavveNavigation.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt index f2dce06..de87048 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt @@ -1,10 +1,9 @@ package com.sopt.presentation.ui.navigation +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -73,9 +72,11 @@ fun WavveNavigation( navController = navController, startDestination = Routes.Auth.Graph, enterTransition = { - slideInVertically { it } + fadeIn() + //slideInVertically { it } + fadeIn() + EnterTransition.None }, exitTransition = { - fadeOut() + //fadeOut() + ExitTransition.None } ) { navigation( From 8afb1dcdf3239b82ff3df9e795390a436aa3ead5 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 15:59:37 +0900 Subject: [PATCH 36/41] =?UTF-8?q?refactor:=20Debounce=20search=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/search/composable/SearchScreen.kt | 51 ++++++++----------- .../search/viewmodel/SearchViewModel.kt | 47 +++++++++-------- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt index 47ced17..e43a125 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt @@ -1,6 +1,5 @@ package com.sopt.presentation.ui.screen.search.composable -import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -12,27 +11,27 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.presentation.R import com.sopt.presentation.ui.component.surface.DefaultSurface +import com.sopt.presentation.ui.component.text.PrimaryText import com.sopt.presentation.ui.component.textfield.BottomLinedTextField import com.sopt.presentation.ui.screen.search.viewmodel.SearchResultUiState import com.sopt.presentation.ui.screen.search.viewmodel.SearchViewModel import com.sopt.presentation.ui.theme.WavveTheme import com.sopt.presentation.ui.util.noRippleClickable -import kotlinx.coroutines.flow.collect @Composable fun SearchScreen( @@ -41,9 +40,10 @@ fun SearchScreen( ) { val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() - val searchedHobby by viewModel.searchedHobby.collectAsStateWithLifecycle(SearchResultUiState.Nothing) + val searchedHobby by viewModel.searchedHobby.collectAsStateWithLifecycle() DefaultSurface( modifier = modifier @@ -60,9 +60,10 @@ fun SearchScreen( onValueChange = viewModel::onSearchQueryChanged, placeholder = stringResource(R.string.search_placeholder), keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Search + imeAction = ImeAction.Search, + keyboardType = KeyboardType.Number ), keyboardActions = KeyboardActions( - onSearch = { viewModel.search(searchQuery) } + onSearch = { keyboardController?.hide() } ), leadingIcon = { Icon( @@ -86,16 +87,19 @@ fun SearchScreen( } ) - when { - searchedHobby is SearchResultUiState.Success -> { - val hobby = (searchedHobby as SearchResultUiState.Success).hobby - Box( - modifier = Modifier.fillMaxWidth().padding(32.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "너의 취미는 $hobby", - ) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + when { + searchedHobby is SearchResultUiState.Success -> { + val hobby = (searchedHobby as SearchResultUiState.Success).hobby + PrimaryText(text = "너의 취미는 $hobby",) + } + searchedHobby is SearchResultUiState.NotExistUser -> { + PrimaryText(text = "존재하지 않는 사용자입니다.",) } } } @@ -107,17 +111,4 @@ fun SearchScreen( ) } } - - LaunchedEffect(Unit) { - viewModel.searchedHobby.collect { - when { - it is SearchResultUiState.InputEmpty -> - Toast.makeText(context, context.getString(R.string.require_search_input), Toast.LENGTH_SHORT).show() - it is SearchResultUiState.InputNotNumber -> - Toast.makeText(context, context.getString(R.string.require_only_number), Toast.LENGTH_SHORT).show() - it is SearchResultUiState.NotExistUser -> - Toast.makeText(context, context.getString(R.string.not_exist_search_result), Toast.LENGTH_SHORT).show() - } - } - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt index 0d8e9ee..77a10b0 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt @@ -7,20 +7,20 @@ import com.sopt.domain.exception.SearchHobbyError import com.sopt.domain.usecase.SearchHobbyUseCase import com.sopt.presentation.ui.state.VideoOverviewViewState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transform import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, private val searchHobbyUseCase: SearchHobbyUseCase ): ViewModel() { - val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") - val displayedVideoOverviews = listOf( VideoOverviewViewState( id = 1, @@ -81,25 +81,29 @@ class SearchViewModel @Inject constructor( ) ) - val searchedHobby: SharedFlow - field = MutableSharedFlow() - - fun onSearchQueryChanged(query: String) { - savedStateHandle[SEARCH_QUERY] = query - } - - fun search(query: String) { - viewModelScope.launch { - searchHobbyUseCase(query).onSuccess { - searchedHobby.emit(SearchResultUiState.Success(it)) + val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") + val searchedHobby: StateFlow = searchQuery.debounce(200) + .distinctUntilChanged() + .transform { query -> + searchHobbyUseCase(query).onSuccess { hobby -> + emit(SearchResultUiState.Success(hobby)) }.onFailure { when (it) { - is SearchHobbyError.InputEmpty -> searchedHobby.emit(SearchResultUiState.InputEmpty) - is SearchHobbyError.InputNotNumber -> searchedHobby.emit(SearchResultUiState.InputNotNumber) - is SearchHobbyError.NotExistUserNo -> searchedHobby.emit(SearchResultUiState.NotExistUser) + is SearchHobbyError.InputEmpty -> emit(SearchResultUiState.InputEmpty) + is SearchHobbyError.InputNotNumber -> emit(SearchResultUiState.InputNotNumber) + is SearchHobbyError.NotExistUserNo -> emit(SearchResultUiState.NotExistUser) } } - } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = SearchResultUiState.InputEmpty + ) + + fun onSearchQueryChanged(query: String) { + query.findLast { it.isDigit().not() }?.let { return } + + savedStateHandle[SEARCH_QUERY] = query } companion object { @@ -108,7 +112,6 @@ class SearchViewModel @Inject constructor( } sealed interface SearchResultUiState { - data object Nothing: SearchResultUiState data class Success(val hobby: String): SearchResultUiState data object InputEmpty: SearchResultUiState data object InputNotNumber: SearchResultUiState From 2ccae1935565b975a9880d074c15ddcd292319c5 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 17:30:49 +0900 Subject: [PATCH 37/41] =?UTF-8?q?add:=20=EC=9E=90=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/local/UserLocalDataSource.kt | 22 +++++++++++++++++++ .../data/repository/UserRepositoryImpl.kt | 11 ++++++++++ .../domain/exception/ExceptionExtensions.kt | 12 ---------- .../java/com/sopt/domain/model/Account.kt | 6 +++++ .../sopt/domain/repository/UserRepository.kt | 3 +++ .../domain/usecase/TryAutoSignInUseCase.kt | 17 ++++++++++++++ .../signin/viewmodel/SignInViewModel.kt | 21 +++++++++++++++++- 7 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 domain/src/main/java/com/sopt/domain/model/Account.kt create mode 100644 domain/src/main/java/com/sopt/domain/usecase/TryAutoSignInUseCase.kt diff --git a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt index 893155b..bfa8b28 100644 --- a/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/local/UserLocalDataSource.kt @@ -2,10 +2,32 @@ package com.sopt.data.datasource.local import android.content.SharedPreferences import com.sopt.data.UserSharedPref +import com.sopt.domain.model.Account import javax.inject.Inject class UserLocalDataSource @Inject constructor( @UserSharedPref private val userSharedPreferences: SharedPreferences, ) { + fun saveAccount(account: Account) { + userSharedPreferences.edit() + .putString(KEY_USERNAME, account.username) + .putString(KEY_PASSWORD, account.password) + .apply() + } + + fun getAccount(): Account? { + val username = userSharedPreferences.getString(KEY_USERNAME, null) + val password = userSharedPreferences.getString(KEY_PASSWORD, null) + return if (username != null && password != null) { + Account(username, password) + } else { + null + } + } + + companion object { + private const val KEY_USERNAME = "username" + private const val KEY_PASSWORD = "password" + } } \ No newline at end of file diff --git a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt index dcef000..f518481 100644 --- a/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repository/UserRepositoryImpl.kt @@ -11,6 +11,7 @@ import com.sopt.domain.exception.SearchHobbyError import com.sopt.domain.exception.SignInError import com.sopt.domain.exception.SignUpError import com.sopt.domain.exception.runCatchingByCode +import com.sopt.domain.model.Account import com.sopt.domain.repository.UserRepository import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -77,4 +78,14 @@ class UserRepositoryImpl @Inject constructor( } } } + + override suspend fun saveAccount(account: Account): Result { + return runCatchingByCode { + userLocalDataSource.saveAccount(account) + } + } + + override suspend fun getAccount(): Account? { + return userLocalDataSource.getAccount() + } } diff --git a/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt b/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt index f4cae5e..c40ab17 100644 --- a/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt +++ b/domain/src/main/java/com/sopt/domain/exception/ExceptionExtensions.kt @@ -17,16 +17,4 @@ suspend fun T.runCatchingByCode( } catch (e: Throwable) { Result.failure(e) } -} - -suspend fun T.runSuspendCatching( - block: suspend T.() -> R -): Result { - return try { - Result.success(block()) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - Result.failure(e) - } } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/model/Account.kt b/domain/src/main/java/com/sopt/domain/model/Account.kt new file mode 100644 index 0000000..c4e6eac --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/model/Account.kt @@ -0,0 +1,6 @@ +package com.sopt.domain.model + +data class Account( + val username: String, + val password: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt index 58bdb85..1456230 100644 --- a/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/UserRepository.kt @@ -1,5 +1,6 @@ package com.sopt.domain.repository +import com.sopt.domain.model.Account import kotlinx.coroutines.flow.Flow interface UserRepository { @@ -8,4 +9,6 @@ interface UserRepository { fun fetchMyHobby(): Flow suspend fun fetchUserHobby(no: Int): Result suspend fun updateProfile(password: String, hobby: String): Result + suspend fun saveAccount(account: Account): Result + suspend fun getAccount(): Account? } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/usecase/TryAutoSignInUseCase.kt b/domain/src/main/java/com/sopt/domain/usecase/TryAutoSignInUseCase.kt new file mode 100644 index 0000000..a7281da --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/usecase/TryAutoSignInUseCase.kt @@ -0,0 +1,17 @@ +package com.sopt.domain.usecase + +import com.sopt.domain.repository.UserRepository +import javax.inject.Inject + +class TryAutoSignInUseCase @Inject constructor( + private val userRepository: UserRepository +) { + + suspend operator fun invoke(): Result { + userRepository.getAccount()?.let { account -> + return userRepository.signIn(account.username, account.password) + } ?: run { + return Result.failure(Exception("No account")) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt index a424ac4..801915e 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt @@ -3,18 +3,28 @@ package com.sopt.presentation.ui.screen.signin.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.domain.exception.SignInError +import com.sopt.domain.model.Account +import com.sopt.domain.repository.UserRepository import com.sopt.domain.usecase.SignInUseCase +import com.sopt.domain.usecase.TryAutoSignInUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SignInViewModel @Inject constructor( - private val signInUseCase: SignInUseCase + private val signInUseCase: SignInUseCase, + private val tryAutoSignInUseCase: TryAutoSignInUseCase, + private val userRepository: UserRepository ) : ViewModel() { val usernameInput: StateFlow @@ -33,10 +43,19 @@ class SignInViewModel @Inject constructor( passwordInput.value = password } + init { + viewModelScope.launch { + tryAutoSignInUseCase().onSuccess { + signInUiState.emit(SignInUiState.Success) + } + } + } + fun trySignIn() { viewModelScope.launch { signInUseCase(usernameInput.value, passwordInput.value).onSuccess { signInUiState.emit(SignInUiState.Success) + userRepository.saveAccount(Account(usernameInput.value, passwordInput.value)) }.onFailure { when (it) { is SignInError.UsernameInputEmpty -> signInUiState.emit(SignInUiState.UsernameInputEmpty) From 902a06e5f3d4a59fed892bdb4ee0c9709f3e4fa0 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 17:50:24 +0900 Subject: [PATCH 38/41] =?UTF-8?q?chore:=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EB=AA=85=EB=A3=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/search/composable/SearchScreen.kt | 7 +- .../search/viewmodel/SearchViewModel.kt | 128 +++++++++--------- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt index e43a125..cb7a3ba 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt @@ -43,7 +43,7 @@ fun SearchScreen( val keyboardController = LocalSoftwareKeyboardController.current val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() - val searchedHobby by viewModel.searchedHobby.collectAsStateWithLifecycle() + val searchedHobby by viewModel.searchedHobbyUiState.collectAsStateWithLifecycle() DefaultSurface( modifier = modifier @@ -96,10 +96,11 @@ fun SearchScreen( when { searchedHobby is SearchResultUiState.Success -> { val hobby = (searchedHobby as SearchResultUiState.Success).hobby - PrimaryText(text = "너의 취미는 $hobby",) + PrimaryText(text = "너의 취미는 $hobby") } + searchedHobby is SearchResultUiState.NotExistUser -> { - PrimaryText(text = "존재하지 않는 사용자입니다.",) + PrimaryText(text = "존재하지 않는 사용자입니다.") } } } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt index 77a10b0..825245a 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt @@ -19,70 +19,70 @@ import javax.inject.Inject class SearchViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val searchHobbyUseCase: SearchHobbyUseCase -): ViewModel() { +) : ViewModel() { val displayedVideoOverviews = listOf( - VideoOverviewViewState( - id = 1, - title = "나는 SOLO", - titleImage = "https://i.namu.wiki/i/naUet4gLa7vaSW9sEH3miT1LNxdoD_CxrjCBYR5BAmEmSDOWNf-rELb9ioqiaMyW7IBEpG0DxRLHvYWhBCkw6Q.webp", - description = "sdsds" - ), - VideoOverviewViewState( - id = 1, - title = "라디오스타", - titleImage = "https://img.imbc.com/template/2023/09/program_36b7867b-0798-44b9-827c-e27b2bcc79c2.jpg", - description = "ㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹ" - ), VideoOverviewViewState( - id = 1, - title = "고딩엄빠", - titleImage = "https://img.mbn.co.kr/program/895/2022/02/23/164557455315.jpg", - description = "ㅎㅀㅎㅎㅎㅎㅎㅎㅎㅎㅎ" - ), VideoOverviewViewState( - id = 1, - title = "골 때리는 그녀들", - titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2023/08/17/6uv1692260719254.jpg", - description = "금쪽ㅇㅈㅇㅈ이" - ), VideoOverviewViewState( - id = 1, - title = "백종원의 골목식당", - titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2021/05/11/enN1620692371424.jpg", - description = "ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ" - ), VideoOverviewViewState( - id = 1, - title = "지옥에서 온 판사", - titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2024/09/02/ejm1725238865329-640-360.jpg", - description = "ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ" - ), VideoOverviewViewState( - id = 1, - title = "개소리", - titleImage = "https://programres.kbs.co.kr/t2024-0471/2024/8/28/1724810940534_536600.jpg", - description = "ㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹ" - ), VideoOverviewViewState( - id = 1, - title = "런닝맨", - titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2024/08/16/eWM1723784366763.jpg", - description = "ㅈㅇㅈㅇ" - ), VideoOverviewViewState( - id = 1, - title = "나 혼자 산다", - titleImage = "https://i.namu.wiki/i/jUG8Huo01jsCzBPmr7GvebPacH3ULreD9rvSfc1L7hhFwREWVreTtnYTS_z-BD-7vvnSNjh_lSkb4bjtz_ZN9g.webp", - description = "ㄴ" - ), VideoOverviewViewState( - id = 1, - title = "이토록 친밀한 배신자", - titleImage = "https://img.imbc.com/template/2024/09/program_c5f4923a-7720-450d-88a4-990e455e81ab.jpg", - description = "ㄴㅇㄴㅇ" - ), VideoOverviewViewState( - id = 1, - title = "강철부대", - titleImage = "https://image.tving.com/ntgs/contents/CTC/caip/CAIP1500/ko/20240920/0511/P001761839.jpg/dims/resize/1280", - description = "ㅗㅗㅗㅗㅗ" - ) + VideoOverviewViewState( + id = 1, + title = "나는 SOLO", + titleImage = "https://i.namu.wiki/i/naUet4gLa7vaSW9sEH3miT1LNxdoD_CxrjCBYR5BAmEmSDOWNf-rELb9ioqiaMyW7IBEpG0DxRLHvYWhBCkw6Q.webp", + description = "sdsds" + ), + VideoOverviewViewState( + id = 1, + title = "라디오스타", + titleImage = "https://img.imbc.com/template/2023/09/program_36b7867b-0798-44b9-827c-e27b2bcc79c2.jpg", + description = "ㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹ" + ), VideoOverviewViewState( + id = 1, + title = "고딩엄빠", + titleImage = "https://img.mbn.co.kr/program/895/2022/02/23/164557455315.jpg", + description = "ㅎㅀㅎㅎㅎㅎㅎㅎㅎㅎㅎ" + ), VideoOverviewViewState( + id = 1, + title = "골 때리는 그녀들", + titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2023/08/17/6uv1692260719254.jpg", + description = "금쪽ㅇㅈㅇㅈ이" + ), VideoOverviewViewState( + id = 1, + title = "백종원의 골목식당", + titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2021/05/11/enN1620692371424.jpg", + description = "ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ" + ), VideoOverviewViewState( + id = 1, + title = "지옥에서 온 판사", + titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2024/09/02/ejm1725238865329-640-360.jpg", + description = "ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ" + ), VideoOverviewViewState( + id = 1, + title = "개소리", + titleImage = "https://programres.kbs.co.kr/t2024-0471/2024/8/28/1724810940534_536600.jpg", + description = "ㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹ" + ), VideoOverviewViewState( + id = 1, + title = "런닝맨", + titleImage = "https://img2.sbs.co.kr/img/sbs_cms/WE/2024/08/16/eWM1723784366763.jpg", + description = "ㅈㅇㅈㅇ" + ), VideoOverviewViewState( + id = 1, + title = "나 혼자 산다", + titleImage = "https://i.namu.wiki/i/jUG8Huo01jsCzBPmr7GvebPacH3ULreD9rvSfc1L7hhFwREWVreTtnYTS_z-BD-7vvnSNjh_lSkb4bjtz_ZN9g.webp", + description = "ㄴ" + ), VideoOverviewViewState( + id = 1, + title = "이토록 친밀한 배신자", + titleImage = "https://img.imbc.com/template/2024/09/program_c5f4923a-7720-450d-88a4-990e455e81ab.jpg", + description = "ㄴㅇㄴㅇ" + ), VideoOverviewViewState( + id = 1, + title = "강철부대", + titleImage = "https://image.tving.com/ntgs/contents/CTC/caip/CAIP1500/ko/20240920/0511/P001761839.jpg/dims/resize/1280", + description = "ㅗㅗㅗㅗㅗ" ) + ) val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") - val searchedHobby: StateFlow = searchQuery.debounce(200) + val searchedHobbyUiState: StateFlow = searchQuery.debounce(200) .distinctUntilChanged() .transform { query -> searchHobbyUseCase(query).onSuccess { hobby -> @@ -105,15 +105,15 @@ class SearchViewModel @Inject constructor( savedStateHandle[SEARCH_QUERY] = query } - + companion object { const val SEARCH_QUERY = "searchQuery" } } sealed interface SearchResultUiState { - data class Success(val hobby: String): SearchResultUiState - data object InputEmpty: SearchResultUiState - data object InputNotNumber: SearchResultUiState - data object NotExistUser: SearchResultUiState + data class Success(val hobby: String) : SearchResultUiState + data object InputEmpty : SearchResultUiState + data object InputNotNumber : SearchResultUiState + data object NotExistUser : SearchResultUiState } \ No newline at end of file From d2ec5b0f08c1c9380fe2bc96efe03f20c641309b Mon Sep 17 00:00:00 2001 From: Thirfir Date: Tue, 12 Nov 2024 18:43:01 +0900 Subject: [PATCH 39/41] =?UTF-8?q?chore:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...orkCallAdapter.kt => ResponseConverter.kt} | 0 .../sopt/presentation/ui/navigation/Routes.kt | 33 ++++++++++++------- .../ui/navigation/WavveNavigation.kt | 2 +- .../HeadDisplayedVideoHorizontalPager.kt | 10 +++--- .../home/composable/HomeContentScreen.kt | 8 ++--- .../ui/screen/home/composable/HomeScreen.kt | 2 +- .../ui/state/VideoOverviewViewState.kt | 4 +-- .../presentation/ui/util/AndroidExtensions.kt | 11 +++---- 8 files changed, 39 insertions(+), 31 deletions(-) rename app/src/main/java/org/sopt/and/adapter/{NetworkCallAdapter.kt => ResponseConverter.kt} (100%) diff --git a/app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt b/app/src/main/java/org/sopt/and/adapter/ResponseConverter.kt similarity index 100% rename from app/src/main/java/org/sopt/and/adapter/NetworkCallAdapter.kt rename to app/src/main/java/org/sopt/and/adapter/ResponseConverter.kt diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt index 41118aa..59e3c75 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/Routes.kt @@ -6,25 +6,36 @@ import kotlinx.serialization.Serializable object Routes { object Auth { - @Serializable object Graph - @Serializable object SignIn - @Serializable object SignUp + @Serializable + object Graph + @Serializable + object SignIn + @Serializable + object SignUp } object Main { - @Serializable object Graph - @Serializable object Home - @Serializable object Search - @Serializable object My + @Serializable + object Graph + @Serializable + object Home + @Serializable + data object Search + @Serializable + object My } object VideoDetail { - @Serializable object Graph - @Serializable data class Video(val videoOverviewViewState: VideoOverviewViewState) + @Serializable + object Graph + @Serializable + data class Video(val videoOverviewViewState: VideoOverviewViewState) } object MySetting { - @Serializable object Graph - @Serializable object EditProfile + @Serializable + object Graph + @Serializable + object EditProfile } } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt index de87048..bf33d3b 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/navigation/WavveNavigation.kt @@ -152,7 +152,7 @@ fun WavveNavigation( } navigation( - startDestination = Routes.VideoDetail.Video(VideoOverviewViewState.Empty) + startDestination = Routes.VideoDetail.Video(VideoOverviewViewState.empty) ) { composable( diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HeadDisplayedVideoHorizontalPager.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HeadDisplayedVideoHorizontalPager.kt index 706e0e2..c183de2 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HeadDisplayedVideoHorizontalPager.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HeadDisplayedVideoHorizontalPager.kt @@ -37,11 +37,11 @@ import com.sopt.presentation.ui.util.noRippleClickable @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun SharedTransitionScope.HeadDisplayedVideoHorizontalPager( - modifier: Modifier = Modifier, state: PagerState, videoOverviews: List, onVideoClicked: (VideoOverviewViewState) -> Unit, - animatedVisibilityScope: AnimatedVisibilityScope + animatedVisibilityScope: AnimatedVisibilityScope, + modifier: Modifier = Modifier ) { HorizontalPager( modifier = modifier, @@ -63,12 +63,12 @@ fun SharedTransitionScope.HeadDisplayedVideoHorizontalPager( @OptIn(ExperimentalGlideComposeApi::class, ExperimentalSharedTransitionApi::class) @Composable fun SharedTransitionScope.HeadDisplayedVideoItem( - modifier: Modifier = Modifier, videoOverview: VideoOverviewViewState, totalPage: Int, currentPage: Int, - onClick: (VideoOverviewViewState) -> Unit = {}, - animatedVisibilityScope: AnimatedVisibilityScope + animatedVisibilityScope: AnimatedVisibilityScope, + modifier: Modifier = Modifier, + onClick: (VideoOverviewViewState) -> Unit = {} ) { Box( modifier = modifier diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeContentScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeContentScreen.kt index 9897692..f553d38 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeContentScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeContentScreen.kt @@ -46,13 +46,13 @@ import kotlinx.coroutines.withContext ) @Composable fun SharedTransitionScope.HomeContentScreen( - modifier: Modifier = Modifier, headVideoOverviews: List, commonVideoOverviews: List, topVideoOverviews: CommonVideoOverviewsViewState, onVideoTypeSelected: (VideoType) -> Unit, onVideoSelected: (VideoOverviewViewState) -> Unit, - animatedVisibilityScope: AnimatedVisibilityScope + animatedVisibilityScope: AnimatedVisibilityScope, + modifier: Modifier = Modifier, ) { val headDisplayPagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { @@ -174,8 +174,8 @@ fun SharedTransitionScope.HomeContentScreen( @Composable private fun VideoTypeTabRow( - modifier: Modifier = Modifier, - onVideoTypeSelected: (VideoType) -> Unit + onVideoTypeSelected: (VideoType) -> Unit, + modifier: Modifier = Modifier ) { Row( modifier = modifier.horizontalScroll(rememberScrollState()), diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeScreen.kt index baab313..c0b6ade 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/home/composable/HomeScreen.kt @@ -17,9 +17,9 @@ import com.sopt.presentation.ui.state.VideoOverviewViewState @Composable fun SharedTransitionScope.HomeScreen( modifier: Modifier = Modifier, + animatedVisibilityScope: AnimatedVisibilityScope, viewModel: HomeViewModel = hiltViewModel(), onNavigateToVideoDetail: (VideoOverviewViewState) -> Unit, - animatedVisibilityScope: AnimatedVisibilityScope ) { DefaultSurface( diff --git a/presentation/src/main/java/com/sopt/presentation/ui/state/VideoOverviewViewState.kt b/presentation/src/main/java/com/sopt/presentation/ui/state/VideoOverviewViewState.kt index 5a8b67b..4739158 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/state/VideoOverviewViewState.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/state/VideoOverviewViewState.kt @@ -13,10 +13,10 @@ data class VideoOverviewViewState( val title: String, val titleImage: String, val description: String, -): Parcelable { +) : Parcelable { companion object { - val Empty = VideoOverviewViewState( + val empty = VideoOverviewViewState( id = 0, title = "", titleImage = "", diff --git a/presentation/src/main/java/com/sopt/presentation/ui/util/AndroidExtensions.kt b/presentation/src/main/java/com/sopt/presentation/ui/util/AndroidExtensions.kt index 9be56a4..cae9c41 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/util/AndroidExtensions.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/util/AndroidExtensions.kt @@ -1,12 +1,9 @@ package com.sopt.presentation.ui.util -import android.os.Build import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.BundleCompat -internal inline fun Bundle.getParcelableCompat(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } +internal inline fun Bundle.getParcelableCompat(key: String): T? { + return BundleCompat.getParcelable(this, key, T::class.java) } \ No newline at end of file From 29c90257e161bc08f044e375194fda660eb51b3d Mon Sep 17 00:00:00 2001 From: Thirfir Date: Thu, 14 Nov 2024 15:59:18 +0900 Subject: [PATCH 40/41] =?UTF-8?q?add:=20=EA=B2=80=EC=83=89=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/search/composable/SearchScreen.kt | 17 ++++++++++------- .../screen/search/viewmodel/SearchViewModel.kt | 16 ++++++++++------ .../screen/signin/viewmodel/SignInViewModel.kt | 4 ++-- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt index cb7a3ba..023f0c6 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/composable/SearchScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -43,7 +44,7 @@ fun SearchScreen( val keyboardController = LocalSoftwareKeyboardController.current val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle() - val searchedHobby by viewModel.searchedHobbyUiState.collectAsStateWithLifecycle() + val searchedHobbyUiState by viewModel.searchedHobbyUiState.collectAsStateWithLifecycle() DefaultSurface( modifier = modifier @@ -94,13 +95,15 @@ fun SearchScreen( contentAlignment = Alignment.Center ) { when { - searchedHobby is SearchResultUiState.Success -> { - val hobby = (searchedHobby as SearchResultUiState.Success).hobby - PrimaryText(text = "너의 취미는 $hobby") + searchedHobbyUiState is SearchResultUiState.Success -> { + val hobby = (searchedHobbyUiState as SearchResultUiState.Success).hobby + PrimaryText(text = "너의 취미는 $hobby",) } - - searchedHobby is SearchResultUiState.NotExistUser -> { - PrimaryText(text = "존재하지 않는 사용자입니다.") + searchedHobbyUiState is SearchResultUiState.NotExistUser -> { + PrimaryText(text = "존재하지 않는 사용자입니다.",) + } + searchedHobbyUiState is SearchResultUiState.Loading -> { + CircularProgressIndicator() } } } diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt index 825245a..b515826 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/search/viewmodel/SearchViewModel.kt @@ -7,6 +7,7 @@ import com.sopt.domain.exception.SearchHobbyError import com.sopt.domain.usecase.SearchHobbyUseCase import com.sopt.presentation.ui.state.VideoOverviewViewState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce @@ -19,7 +20,7 @@ import javax.inject.Inject class SearchViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val searchHobbyUseCase: SearchHobbyUseCase -) : ViewModel() { +): ViewModel() { val displayedVideoOverviews = listOf( VideoOverviewViewState( @@ -82,9 +83,11 @@ class SearchViewModel @Inject constructor( ) val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") - val searchedHobbyUiState: StateFlow = searchQuery.debounce(200) + val searchedHobbyUiState: StateFlow = searchQuery.debounce(300) .distinctUntilChanged() .transform { query -> + emit(SearchResultUiState.Loading) + delay(200) searchHobbyUseCase(query).onSuccess { hobby -> emit(SearchResultUiState.Success(hobby)) }.onFailure { @@ -112,8 +115,9 @@ class SearchViewModel @Inject constructor( } sealed interface SearchResultUiState { - data class Success(val hobby: String) : SearchResultUiState - data object InputEmpty : SearchResultUiState - data object InputNotNumber : SearchResultUiState - data object NotExistUser : SearchResultUiState + data class Success(val hobby: String): SearchResultUiState + data object Loading: SearchResultUiState + data object InputEmpty: SearchResultUiState + data object InputNotNumber: SearchResultUiState + data object NotExistUser: SearchResultUiState } \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt index 801915e..b4d6f49 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/signin/viewmodel/SignInViewModel.kt @@ -50,12 +50,12 @@ class SignInViewModel @Inject constructor( } } } - + fun trySignIn() { viewModelScope.launch { signInUseCase(usernameInput.value, passwordInput.value).onSuccess { - signInUiState.emit(SignInUiState.Success) userRepository.saveAccount(Account(usernameInput.value, passwordInput.value)) + signInUiState.emit(SignInUiState.Success) }.onFailure { when (it) { is SignInError.UsernameInputEmpty -> signInUiState.emit(SignInUiState.UsernameInputEmpty) From 53a1f6eecc28ee1d416527ca8c9ecb69418469c6 Mon Sep 17 00:00:00 2001 From: Thirfir Date: Thu, 14 Nov 2024 16:00:14 +0900 Subject: [PATCH 41/41] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B9=85=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt index d0798d1..c822fe6 100644 --- a/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/ui/screen/my/viewmodel/EditProfileViewModel.kt @@ -55,10 +55,8 @@ class EditProfileViewModel @Inject constructor( fun updateProfile() { viewModelScope.launch { updateProfileUseCase(passwordInput.value, hobbyInput.value).onSuccess { - println("dddddddd $it") updateProfileUiState.emit(UpdateProfileUiState.Success) }.onFailure { - println("dddddddd2 $it") when(it) { is UpdateProfileError.PasswordInputEmpty -> updateProfileUiState.emit(UpdateProfileUiState.PasswordInputEmpty) is UpdateProfileError.HobbyInputEmpty -> updateProfileUiState.emit(UpdateProfileUiState.HobbyInputEmpty)