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

Week4 필수과제 구현 #8

Open
wants to merge 8 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
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ dependencies {
// Accompanist
implementation(libs.accompanist.pager)
implementation(libs.accompanist.pager.indicators)
implementation(libs.androidx.datastore.core.android)
implementation(libs.androidx.datastore.preferences.core.jvm)

// Testing
testImplementation(libs.junit)
Expand All @@ -94,4 +96,8 @@ dependencies {
implementation(libs.retrofit)
implementation(libs.retrofit.kotlin.serialization.converter)
implementation(libs.kotlinx.serialization.json)

// DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.datastore:datastore-preferences-core:1.0.0")
Comment on lines +101 to +102
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 버전 카탈로그 적용해주시면 좋을 것 같네요!

}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ANDANDROID"
android:usesCleartextTraffic="true"
tools:targetApi="31">

<activity
Expand Down
20 changes: 12 additions & 8 deletions app/src/main/java/org/sopt/and/Route.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package org.sopt.and

import kotlinx.serialization.Serializable


@Serializable
sealed class Route(val route: String) {
@Serializable
data object Home : Route("home")

data class SignIn(val email: String = "", val password: String = "") :
Route("signIn?email={email}&password={password}") {
fun createRoute(email: String = "", password: String = "") =
"signIn?email=$email&password=$password"
}
@Serializable
data object SignIn : Route("signIn")

@Serializable
data object SignUp : Route("signUp")

@Serializable
data object Search : Route("search")

data class MyPage(val email: String) : Route("myPage?email={email}") {
fun createRoute(email: String) = "myPage?email=$email"
}
@Serializable
data object MyPage : Route("myPage")
}
32 changes: 32 additions & 0 deletions app/src/main/java/org/sopt/and/data/ServicePool.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// data/ServicePool.kt
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.service.AuthService
import retrofit2.Retrofit

object ServicePool {
private val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
}

private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(okHttpClient)
.build()

val authService: AuthService by lazy { retrofit.create(AuthService::class.java) }
}
43 changes: 43 additions & 0 deletions app/src/main/java/org/sopt/and/data/local/AuthLocalDataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.sopt.and.data.local

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

private val Context.dataStore by preferencesDataStore(name = "auth")

class AuthLocalDataSource(private val context: Context) {
private val tokenKey = stringPreferencesKey("token")

suspend fun saveToken(token: String) {
context.dataStore.edit { preferences ->
preferences[tokenKey] = token
}
}

fun getToken(): Flow<String?> = context.dataStore.data.map { preferences ->
preferences[tokenKey]
}

suspend fun clearToken() {
Copy link
Contributor

Choose a reason for hiding this comment

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

일반 함수와 suspend 함수의 차이는 무엇일까요?

context.dataStore.edit { preferences ->
preferences.remove(tokenKey)
}
}

companion object {
@Volatile
private var instance: AuthLocalDataSource? = null

fun getInstance(context: Context): AuthLocalDataSource {
return instance ?: synchronized(this) {
instance ?: AuthLocalDataSource(context.applicationContext).also {
instance = it
}
}
}
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/org/sopt/and/data/model/request/AuthRequest.kt

Choose a reason for hiding this comment

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

AuthRequest 파일 안에 SignUpRequest, SignInRequest 을 한번데 넣어서 관리하시는군요..! 나중에 유저 인증 관련 기능이 더 복잡해질 걸 생각하면, 이런 관리 방식은 굉장히 효율적인 것 같아요!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.sopt.and.data.model.request

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

@Serializable
data class SignUpRequest(
@SerialName("username")
val username: String,
@SerialName("password")
val password: String,
@SerialName("hobby")
val hobby: String
)

@Serializable
data class SignInRequest(
@SerialName("username")
val username: String,
@SerialName("password")
val password: String
)
30 changes: 30 additions & 0 deletions app/src/main/java/org/sopt/and/data/model/response/AuthResponse.kt

Choose a reason for hiding this comment

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

AuthResponse 안에 응답 관련 클래스도 다 넣어두니 훨씬 깔끔하네요!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.sopt.and.data.model.response

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

@Serializable
data class BaseResponse<T>(
@SerialName("result")
val result: T? = null,
@SerialName("code")
val code: String? = null
)
Comment on lines +7 to +12

Choose a reason for hiding this comment

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

BaseResponse 쓰는거 좋은데요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

명세서에 겹치는 부분은 이렇게 만드는게 좋다고 하더라구요! 그래서 적용해봄쓰


@Serializable
data class SignUpResponse(
@SerialName("no")
val no: Int
)

@Serializable
data class SignInResponse(
@SerialName("token")
val token: String
)

@Serializable
data class HobbyResponse(
@SerialName("hobby")
val hobby: String
)
30 changes: 30 additions & 0 deletions app/src/main/java/org/sopt/and/data/service/AuthService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.sopt.and.data.service

import org.sopt.and.data.model.request.SignInRequest
import org.sopt.and.data.model.request.SignUpRequest
import org.sopt.and.data.model.response.BaseResponse
import org.sopt.and.data.model.response.HobbyResponse
import org.sopt.and.data.model.response.SignInResponse
import org.sopt.and.data.model.response.SignUpResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST

interface AuthService {
@POST("user")
suspend fun signUp(
@Body request: SignUpRequest
): Response<BaseResponse<SignUpResponse>>

@POST("login")
suspend fun signIn(
@Body request: SignInRequest
): Response<BaseResponse<SignInResponse>>

@GET("user/my-hobby")
suspend fun getMyHobby(
@Header("token") token: String
): Response<BaseResponse<HobbyResponse>>
}
5 changes: 3 additions & 2 deletions app/src/main/java/org/sopt/and/domain/User.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.sopt.and.domain

data class User(
var email: String = "",
var password: String = ""
val username: String = "",
val password: String = "",
val hobby: String = "" // hobby 필드 추가
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,6 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun EmailInputField(
value: String,
placeholder: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
isError: Boolean = false,
errorMessage: String? = null
) {
val containerColor by animateColorAsState(
targetValue = if (isError) Color.DarkGray.copy(red = 0.4f) else Color.DarkGray,
label = "containerColor"
)

Column {
TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier
.fillMaxWidth()
.height(60.dp),
placeholder = {
Text(
text = placeholder,
color = Color.Gray
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = containerColor,
unfocusedContainerColor = containerColor,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedIndicatorColor = if (isError) Color.Red.copy(alpha = 0.5f) else Color.Transparent,
unfocusedIndicatorColor = if (isError) Color.Red.copy(alpha = 0.5f) else Color.Transparent,
errorContainerColor = containerColor,
errorIndicatorColor = Color.Red.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(5.dp),
singleLine = true,
isError = isError
)

if (isError && errorMessage != null) {
Text(
text = errorMessage,
color = Color.Red.copy(alpha = 0.8f),
fontSize = 12.sp,
modifier = Modifier.padding(start = 4.dp, top = 4.dp)
)
}
}
}

@Composable
fun PasswordInputField(
value: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// presentation/component/TextInputField.kt
package org.sopt.and.presentation.component

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun TextInputField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: String = "",
isError: Boolean = false,
maxLength: Int = 8,
singleLine: Boolean = true
) {
TextField(
value = value,
onValueChange = {
if (it.length <= maxLength) {
onValueChange(it)
}
},
modifier = modifier
.fillMaxWidth()
.height(60.dp),
placeholder = {
Text(
text = placeholder,
color = Color.Gray
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.DarkGray,
unfocusedContainerColor = Color.DarkGray,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedIndicatorColor = if (isError) Color.Red.copy(alpha = 0.5f) else Color.Transparent,
unfocusedIndicatorColor = if (isError) Color.Red.copy(alpha = 0.5f) else Color.Transparent,
errorContainerColor = Color.DarkGray,
errorIndicatorColor = Color.Red.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(5.dp),
singleLine = singleLine,
isError = isError
)
}
18 changes: 6 additions & 12 deletions app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.sopt.and.presentation.main

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
Expand All @@ -17,26 +16,21 @@ import org.sopt.and.presentation.navigation.NavGraph
@Composable
fun MainScreen() {
val navController = rememberNavController()
var bottomNaviVisible by remember { mutableStateOf(false) }
var userEmail by remember { mutableStateOf("") }
var isLoggedIn by remember { mutableStateOf(false) }
Copy link
Contributor

Choose a reason for hiding this comment

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

변수명에서 조금 더 고민을 해보면 좋을 것 같습니다.
현재는 로그인을 한다면 바텀바가 보이고 있지만, 추후에는 조건이 달라질 수 있어요.

그렇기 때문에 isLoggedIn과 같이 조건이 적혀있는 변수명을 사용한다면 추후 조건이 변경되었을 때 변수명도 변경해야 하는 경우가 생깁니다


Scaffold(
bottomBar = {
if (bottomNaviVisible) {
if (isLoggedIn) {
BottomNavigationBar(
navController = navController,
userEmail = userEmail
navController = navController
)
}
}
) { innerPadding: PaddingValues ->
) { innerPadding ->
NavGraph(
navController = navController,
isLogined = { isLogined ->
bottomNaviVisible = isLogined
},
onEmailUpdated = { email ->
userEmail = email
isLoggedIn = { loggedIn ->
isLoggedIn = loggedIn
},
modifier = Modifier.padding(innerPadding)
)
Expand Down
Loading