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

[Feat] Week4 필수 과제 #9

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
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 {
Expand All @@ -16,6 +23,7 @@ android {
versionName = "1.0"

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

buildTypes {
Expand All @@ -36,10 +44,18 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

dependencies {
// 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)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<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:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -11,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ANDANDROID"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".feature.main.MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ fun ErrorDialog(
onClick: () -> Unit,
isEmailError: String,
isPasswordError: String,
isHobbyError: String,
modifier: Modifier = Modifier
) {
Dialog(
Expand All @@ -40,9 +41,21 @@ fun ErrorDialog(
textAlign = TextAlign.Center,
color = Color.Black,
text = when {
isEmailError.isNotEmpty() && isPasswordError.isNotEmpty() -> "$isEmailError\n$isPasswordError"
isEmailError.isNotEmpty() && isPasswordError.isNotEmpty() && isHobbyError.isNotEmpty() ->
"$isEmailError\n$isPasswordError\n$isHobbyError"

isEmailError.isNotEmpty() && isPasswordError.isNotEmpty() ->
"$isEmailError\n$isPasswordError"

isEmailError.isNotEmpty() && isHobbyError.isNotEmpty() ->
"$isEmailError\n$isHobbyError"

isPasswordError.isNotEmpty() && isHobbyError.isNotEmpty() ->
"$isPasswordError\n$isHobbyError"

isEmailError.isNotEmpty() -> isEmailError
isPasswordError.isNotEmpty() -> isPasswordError
isHobbyError.isNotEmpty() -> isHobbyError
else -> ""
}
)
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/org/sopt/and/data/ApiFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.sopt.and.data

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.sopt.and.BuildConfig
import org.sopt.and.data.network.service.UserService
import retrofit2.Retrofit

object ApiFactory {
private const val BASE_URL: String = BuildConfig.BASE_URL

private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

private val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()

val retrofit: Retrofit by lazy {

Choose a reason for hiding this comment

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

Lazy 하게 설정한 것 좋은 것 같아요!

Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
}

inline fun <reified T> create(): T = retrofit.create(T::class.java)

Choose a reason for hiding this comment

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

처음 보는 코드여서 어떤 코드인지 찾아봤는데 재사용성이 더 좋아진다고하니 좋은 코드인 것 같아요~!

}

object ServicePool {
val userService = ApiFactory.create<UserService>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.and.data.model.request


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserLoginRequest(
@SerialName("username")
val username: String,
@SerialName("password")
val password: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.and.data.model.request


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserSignUpRequest(

Choose a reason for hiding this comment

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

dto 네이밍 형식을 서로 맞춰주면 좋을 것 같아요!

@SerialName("username")
val username: String,
@SerialName("password")
val password: String,
@SerialName("hobby")
val hobby: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.and.data.model.response


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ResponseUserHobbyDto(
@SerialName("result")
val result: Result
Comment on lines +8 to +10

Choose a reason for hiding this comment

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

형태가 계속 반복되니 baseResponse를 사용해봐도 좋을 듯 합니다.

) {
@Serializable
data class Result(
@SerialName("hobby")
val hobby: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.and.data.model.response


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ResponseUserSignUpDto(
@SerialName("result")
val result: Result
) {
@Serializable
data class Result(
@SerialName("no")
val no: Int
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.and.data.model.response


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ResponseUserTokenDto(
@SerialName("result")
val result: Result
) {
@Serializable
data class Result(
@SerialName("token")
val token: String
)
}
29 changes: 29 additions & 0 deletions app/src/main/java/org/sopt/and/data/network/service/UserService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sopt.and.data.network.service

import org.sopt.and.data.model.response.ResponseUserHobbyDto
import org.sopt.and.data.model.response.ResponseUserSignUpDto
import org.sopt.and.data.model.response.ResponseUserTokenDto
import org.sopt.and.data.model.request.UserLoginRequest
import org.sopt.and.data.model.request.UserSignUpRequest
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST

interface UserService {
@POST("/user")
fun postUserSignUp(
@Body body: UserSignUpRequest
): Call<ResponseUserSignUpDto>

@POST("/login")
fun postUserLogin(
@Body body: UserLoginRequest
): Call<ResponseUserTokenDto>

@GET("/user/my-hobby")
fun getUserHobby(
@Header("token") token: String,
): Call<ResponseUserHobbyDto>
}
49 changes: 22 additions & 27 deletions app/src/main/java/org/sopt/and/feature/login/LoginScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.sopt.and.feature.login

import android.content.Context
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand All @@ -24,8 +25,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.VerticalDivider
Expand All @@ -34,7 +33,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
Expand All @@ -51,36 +49,35 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import kotlinx.coroutines.launch
import org.sopt.and.R
import org.sopt.and.core.designsystem.component.AuthTextField
import org.sopt.and.core.designsystem.component.SocialLoginButtonGroup
import org.sopt.and.core.designsystem.component.WavveLoginButton
import org.sopt.and.data.model.request.UserLoginRequest
import org.sopt.and.feature.main.Routes
import org.sopt.and.ui.theme.WavveTheme
import org.sopt.and.utils.noRippleClickable

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
localEmail: String,
localPassword: String,
navController: NavController,
onLoginSuccess: (String, String) -> Unit,
viewModel: LoginViewModel = viewModel()
) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
val sharedPreferences = context.getSharedPreferences("token", Context.MODE_PRIVATE)
Comment on lines 70 to +71

Choose a reason for hiding this comment

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

!! 사용은 지양합시다 NPE가 발생할 수 있서요

val editor = sharedPreferences.edit()

val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val email by viewModel.email.observeAsState("")
val userName by viewModel.userName.observeAsState("")
val password by viewModel.password.observeAsState("")
val showPassword = remember { mutableStateOf(false) }

Scaffold(modifier = Modifier.fillMaxSize(),
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
CenterAlignedTopAppBar(
title = {
Expand Down Expand Up @@ -109,11 +106,6 @@ fun LoginScreen(
colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = WavveTheme.colors.BackgroundGray)
)
},
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState
)
}
) { innerPadding ->
Box(
modifier = Modifier
Expand All @@ -131,11 +123,11 @@ fun LoginScreen(
AuthTextField(
modifier = Modifier
.fillMaxWidth(),
value = email,
value = userName,
onValueChange = {
viewModel.setEmail(it)
viewModel.setUserName(it)
},
placeholder = stringResource(R.string.email_or_id),
placeholder = stringResource(R.string.placeholder_user_name),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
Expand Down Expand Up @@ -184,16 +176,19 @@ fun LoginScreen(
WavveLoginButton(
onClick = {
viewModel.onLoginClick(

Choose a reason for hiding this comment

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

viewModel의 함수가 중첩돼서 가시성이 조금 떨어지는 것 같아요!
두개 함수를 조금 분리해서 파라미터로 넘기는 방식도 생각해봐주세요!

localEmail = localEmail,
localPassword = localPassword,
onSuccess = { email, password ->
onLoginSuccess(email, password)
},
onFailure = {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.fail_to_login))
onSuccess = { userName, password ->
viewModel.postUserLogin(
context = context,
body = UserLoginRequest(
username = userName,
password = password
),
) { body ->
editor.putString("loginToken", body!!.result.token)

Choose a reason for hiding this comment

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

이 부분도 스트링 추출 해볼까요!

editor.apply()
onLoginSuccess(userName, password)
}
}
},
)

//키보드 내리기
Expand Down
Loading