Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/#7 week04] #8

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
18 changes: 17 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +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.serialization)
}

val properties = Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}

android {
namespace = "org.sopt.and"
compileSdk = 34
Expand All @@ -17,6 +23,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
}

buildTypes {
Expand All @@ -37,6 +44,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

Expand All @@ -62,6 +70,14 @@ dependencies {
implementation(libs.androidx.ui.text)
implementation(libs.androidx.accompanist.systemuicontroller)
implementation(libs.androidx.compose.foundation)
implementation(libs.io.coil.kt )
implementation(libs.io.coil.kt)
implementation(libs.kotlinx.serialization.json)

// Network
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.retrofit)
implementation(libs.retrofit.kotlin.serialization.converter)
implementation(libs.kotlinx.serialization.json)
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fun WavveTextField(
onPasswordVisibilityChange: (() -> Unit)? = null,
errorMessage: String? = null,
onFocusChanged: (Boolean) -> Unit,
onNext: () -> Unit,
onNext: () -> Unit = {},
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
modifier: Modifier = Modifier
) {
Expand Down Expand Up @@ -126,4 +126,4 @@ fun WavveTextField(
)
}
}
}
}
31 changes: 24 additions & 7 deletions app/src/main/java/org/sopt/and/data/DataSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,39 @@ import android.content.SharedPreferences

class DataSource(private val sharedPreferences: SharedPreferences) {

fun saveUserInfo(email: String, password: String) {
fun saveUserInfo(username: String, password: String, hobby: String) {
sharedPreferences.edit().apply {
putString(KEY_USER_EMAIL, email)
putString(KEY_USER_USERNAME, username)
putString(KEY_USER_PASSWORD, password)
putString(KEY_USER_HOBBY, hobby)
apply()
}
}

fun saveUserToken(token: String) {
sharedPreferences.edit().apply {
putString(KEY_USER_TOKEN, token)
putBoolean(KEY_IS_LOGGED_IN, true)
apply()
Comment on lines +17 to 20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        sharedPreferences.edit()
            .putString(KEY_USER_TOKEN, token)
            .putBoolean(KEY_IS_LOGGED_IN, true)
            .apply()

이렇게 사용해도 되고 아니면,

        sharedPreferences.edit {
            putString(KEY_USER_TOKEN, token)
            putBoolean(KEY_IS_LOGGED_IN, true)
        }

이렇게도 사용할 수 있을 것 같아요 !

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 이렇게도 쓸 수 있군요

}
}

fun getEmail(): String? {
return sharedPreferences.getString(KEY_USER_EMAIL, null)
fun getToken(): String? {
return sharedPreferences.getString(KEY_USER_TOKEN, null)
}

fun getUsername(): String? {
return sharedPreferences.getString(KEY_USER_USERNAME, null)
}

fun getPassword(): String? {
return sharedPreferences.getString(KEY_USER_PASSWORD, null)
}

fun getHobby(): String? {
return sharedPreferences.getString(KEY_USER_HOBBY, null)
}

fun isLoggedIn(): Boolean {
return sharedPreferences.getBoolean(KEY_IS_LOGGED_IN, false)
}
Expand All @@ -30,9 +46,10 @@ class DataSource(private val sharedPreferences: SharedPreferences) {
}

companion object {
private const val KEY_USER_EMAIL = "USER_EMAIL"
private const val KEY_USER_USERNAME = "USER_USERNAME"
private const val KEY_USER_PASSWORD = "USER_PASSWORD"
private const val KEY_USER_HOBBY = "USER_HOBBY"
private const val KEY_USER_TOKEN = "USER_TOKEN"
private const val KEY_IS_LOGGED_IN = "IS_LOGGED_IN"
}

}
}
38 changes: 31 additions & 7 deletions app/src/main/java/org/sopt/and/data/UserRepository.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
package org.sopt.and.data

import android.content.Context
import org.sopt.and.network.UserService
import org.sopt.and.network.request.RequestLoginDto
import org.sopt.and.network.request.RequestSignUpDto
import org.sopt.and.network.response.ResponseDto
import org.sopt.and.network.response.ResponseHobbyDto
import retrofit2.Response

class UserRepository(context: Context) {
class UserRepository(
private val userService: UserService,
context: Context) {
private val dataSource = DataSource(context.getSharedPreferences("UserPrefs", Context.MODE_PRIVATE))

fun saveUserInfo(email: String, password: String) {
dataSource.saveUserInfo(email, password)
suspend fun postSignUp(username: String, password: String, hobby: String): Response<ResponseDto> {
val request = RequestSignUpDto(username, password, hobby)
return userService.postSignUp(request)
}
Comment on lines +16 to 19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 간단하게 api 요청 코드를 작성할 수 있군요! 배워갑니당


fun getEmail(): String? {
return dataSource.getEmail()
suspend fun postLogin(username: String, password: String): Response<ResponseDto> {
val request = RequestLoginDto(username, password)
return userService.postLogin(request)
}

fun getPassword(): String? {
return dataSource.getPassword()
suspend fun getHobby(token: String): Response<ResponseHobbyDto> {
return userService.getHobby(token)
}

fun saveUserInfo(username: String, password: String, hobby: String) {
dataSource.saveUserInfo(username, password, hobby)
}

fun saveUserToken(token: String) {
dataSource.saveUserToken(token)
}

fun getUsername(): String? = dataSource.getUsername()
fun getPassword(): String? = dataSource.getPassword()
fun getHobby(): String? = dataSource.getHobby()
fun getToken(): String? = dataSource.getToken()

fun isLoggedIn(): Boolean {
return dataSource.isLoggedIn()
}

fun logout() {
dataSource.clearUserCredentials()
}

}
4 changes: 2 additions & 2 deletions app/src/main/java/org/sopt/and/feature/login/LoginEvent.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.sopt.and.feature.login

sealed class LoginEvent {
data class Success(val email: String) : LoginEvent()
data class Failure(val message: String) : LoginEvent()
data class Success(val token: String) : LoginEvent()
data class Failure(val message: String) : LoginEvent()
}
48 changes: 25 additions & 23 deletions app/src/main/java/org/sopt/and/feature/login/LoginRoute.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ fun LoginRoute(
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

var email by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var ispasswordVisible by remember { mutableStateOf(false) }
var isemailErrorVisible by remember { mutableStateOf(false) }
var ispasswordErrorVisible by remember { mutableStateOf(false) }
var isPasswordVisible by remember { mutableStateOf(false) }
var isUsernameErrorVisible by remember { mutableStateOf(false) }
var isPasswordErrorVisible by remember { mutableStateOf(false) }

LaunchedEffect(viewModel.loginEvent) {
viewModel.loginEvent.collect { event ->
Expand All @@ -82,19 +82,20 @@ fun LoginRoute(
}

LoginScreen(
email = email,
onEmailChange = { email = it },
username = username,
onUsernameChange = { username = it },
password = password,
onPasswordChange = { password = it },
passwordVisible = ispasswordVisible,
onPasswordVisibilityChange = { ispasswordVisible = !ispasswordVisible },
onLoginClick = { viewModel.onLoginClick(email, password) },
passwordErrorVisible = ispasswordErrorVisible,
onEmailFocusChanged = { isFocused ->
isemailErrorVisible = isFocused && email.isEmpty()
passwordVisible = isPasswordVisible,
onPasswordVisibilityChange = { isPasswordVisible = !isPasswordVisible },
onLoginClick = { viewModel.onLoginClick(username, password) },
usernameErrorVisible = isUsernameErrorVisible,
passwordErrorVisible = isPasswordErrorVisible,
onUsernameFocusChanged = { isFocused ->
isUsernameErrorVisible = isFocused && username.isEmpty()
},
onPasswordFocusChanged = { isFocused ->
ispasswordErrorVisible = isFocused && password.isEmpty()
isPasswordErrorVisible = isFocused && password.isEmpty()
},
onNavigateToSignUp = { navController.navigate("signup") },
snackbarHostState = snackbarHostState
Expand All @@ -103,20 +104,21 @@ fun LoginRoute(

@Composable
fun LoginScreen(
email: String,
onEmailChange: (String) -> Unit,
username: String,
onUsernameChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
passwordVisible: Boolean,
onPasswordVisibilityChange: () -> Unit,
onLoginClick: () -> Unit,
usernameErrorVisible: Boolean,
passwordErrorVisible: Boolean,
onEmailFocusChanged: (Boolean) -> Unit,
onUsernameFocusChanged: (Boolean) -> Unit,
onPasswordFocusChanged: (Boolean) -> Unit,
onNavigateToSignUp: () -> Unit,
snackbarHostState: SnackbarHostState
) {
val emailFocusRequester = remember { FocusRequester() }
val usernameFocusRequester = remember { FocusRequester() }
val passwordFocusRequester = remember { FocusRequester() }
val loginButtonFocusRequester = remember { FocusRequester() }

Expand All @@ -139,15 +141,15 @@ fun LoginScreen(
)

WavveTextField(
value = email,
onValueChange = onEmailChange,
placeholder = "이메일 입력",
value = username,
onValueChange = onUsernameChange,
placeholder = "사용자 이름 입력",
onFocusChanged = { isFocused ->
onEmailFocusChanged(isFocused)
onUsernameFocusChanged(isFocused)
},
Comment on lines 147 to 149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onFocusChanged = onUserNameFocusChanged

이렇게 줄일 수 있을 것 같습니다 !

onNext = { passwordFocusRequester.requestFocus() },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = Next),
modifier = Modifier.focusRequester(emailFocusRequester)
modifier = Modifier.focusRequester(usernameFocusRequester)
)

Spacer(modifier = Modifier.height(16.dp))
Expand Down Expand Up @@ -201,4 +203,4 @@ fun LoginScreen(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp)
)
}
}
}
31 changes: 17 additions & 14 deletions app/src/main/java/org/sopt/and/feature/login/LoginViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,25 @@ class LoginViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _loginEvent = MutableSharedFlow<LoginEvent>()
val loginEvent = _loginEvent.asSharedFlow()

fun onLoginClick(email: String, password: String) {
fun onLoginClick(username: String, password: String) {
viewModelScope.launch {
val savedEmail = userRepository.getEmail()
val savedPassword = userRepository.getPassword()

if (email != savedEmail) {
_loginEvent.emit(LoginEvent.Failure("이메일이 다릅니다."))
return@launch
}

if (password != savedPassword) {
_loginEvent.emit(LoginEvent.Failure("비밀번호가 틀렸습니다."))
return@launch
runCatching {
userRepository.postLogin(username, password)
}.onSuccess { response ->
if (response.isSuccessful && response.body() != null) {
val token = response.body()?.result?.token
if (token != null) {
userRepository.saveUserToken(token)
_loginEvent.emit(LoginEvent.Success(token))
} else {
_loginEvent.emit(LoginEvent.Failure("토큰을 받아오지 못했습니다."))
}
Comment on lines +19 to +26

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 if 문 검증과 _loginEvent.emit 을 하기까지의 depth 를 뷰모델을 거치기전에 해소할 수 있으면 좋을 것 같아요! 그저 token을 받거나, 실패의 동작만 처리하는 동작이 있으면 좋을 것 같아요! 어디에서 해당 처리를 해주면 좋을까요!?

} else {
_loginEvent.emit(LoginEvent.Failure("로그인 실패: ${response.errorBody()?.string() ?: "알 수 없는 오류"}"))
}
}.onFailure { exception ->
_loginEvent.emit(LoginEvent.Failure("네트워크 오류 발생: ${exception.message}"))
}

_loginEvent.emit(LoginEvent.Success(email))
}
}
}
20 changes: 16 additions & 4 deletions app/src/main/java/org/sopt/and/feature/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand All @@ -22,6 +24,7 @@ import org.sopt.and.feature.mypage.MyPageViewModel
import org.sopt.and.feature.search.SearchRoute
import org.sopt.and.feature.signup.SignUpRoute
import org.sopt.and.feature.signup.SignUpViewModel
import org.sopt.and.network.ServicePool.userService
import org.sopt.and.ui.theme.ANDANDROIDTheme

class MainActivity : ComponentActivity() {
Expand All @@ -30,19 +33,28 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
userRepository = UserRepository(this)
userRepository = UserRepository(userService, applicationContext)

setContent {
val isLoggedIn = remember { mutableStateOf(userRepository.isLoggedIn()) }

ANDANDROIDTheme {
ChangeStatusBarColor()
val navController = rememberNavController()

navController.addOnDestinationChangedListener { _, _, _ ->
isLoggedIn.value = userRepository.isLoggedIn()
}
Comment on lines +36 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 여부에 따라서 bottomNav 여부를 가져갈 수도 있군요! 특정 화면에서만 BottomNav 가져가도록 하는게 어떨까 싶습니다. (개인적인 의견입니다!)



Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
if (userRepository.isLoggedIn()) {
BottomNavigationBar(navController = navController)
Modifier.navigationBarsPadding()
if (isLoggedIn.value) {
BottomNavigationBar(
navController = navController,
modifier = Modifier.navigationBarsPadding()
)
}
}
) { innerPadding ->
Expand Down
Loading