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 26 commits into
base: develop
Choose a base branch
from
Open

Week4 필수과제 #8

wants to merge 26 commits into from

Conversation

kamja0510
Copy link
Contributor

Related issue 🛠

Work Description ✏️

  • 유저등록 api 연동
  • 로그인 api 연동
  • 내 취미 조회 api 연동

Screenshot 📸

유저가입 로그인 취미조회
4._._.mp4
4._.mp4
4._.mp4

Uncompleted Tasks 😅

  • 다른 사람 취미 조회 api 연동
  • 취미 변경 api 연동
  • 자동로그인 기능
  • Debounce Search

To Reviewers 📢

구현하다가 아리까리한 부분이 있으면 주석을 살짝 달아놨습니다. 통신이 생각보다 만만치 않은거 같습니다.
제때 비동기 처리를 하는지도 잘 모르겠고, header에 토큰을 추가하는 과정이 이렇게 어려운지 몰랐습니다.
다른 분들 코드보고 다른게 먼지 연구해보겠고, 좋은 리뷰 달아주시면 빠르게 반영해보겠습니다!

rememberLazyListState는 별도의 변수 선언없이 적용
Copy link

@greedy0110 greedy0110 left a comment

Choose a reason for hiding this comment

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

안녕하세요 재민님, 네트워크 연결 하시느라고 고생하십니다 ㅎㅎㅎ. 간단하게 생각해볼만한 지점에 코멘트 남겨두었습니다.

.verticalScroll(scrollState)
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = rememberLazyListState()

Choose a reason for hiding this comment

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

HomeScreen에서 rememberLazyListState 객체를 사용할 게 아니라면 굳이 안적어주셔도 되어요. 기본 생성자로 알아서 생성합니다.

Copy link
Contributor Author

@kamja0510 kamja0510 Nov 18, 2024

Choose a reason for hiding this comment

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

사용하는 함수를 뜯어보는 습관을 들이겠습니다! 들어가보니 기본값으로 제공하네요

Comment on lines +73 to +76
items(
count = genres.size,
key = { genres[it] }
) { index ->

Choose a reason for hiding this comment

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

items 확장함수 중에 List 타입을 인자로 넣으면 T 타입을 그대로 뱉어주는 함수가 있어요. 아래처럼 해도 같은 동작할 수 있습니다. (import 주의)

Suggested change
items(
count = genres.size,
key = { genres[it] }
) { index ->
items(genres) { genre ->

Copy link
Contributor Author

Choose a reason for hiding this comment

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

세미나에서 이렇게 배웠었는데 안되었던 이유가 import를 잘못해서였군요 ㅠ

Comment on lines +78 to +79
myInfoViewModel = TODO(),
myInfoUiState = TODO()

Choose a reason for hiding this comment

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

Preview 안보일 것 같네요. Preview 대상 Composable 에는 ViewModel 인자를 아예 제거하심이 좋습니다.

}

object ServicePool {
fun userService(context: Context) = ApiFactory.create<UserService>(context)
Copy link

@greedy0110 greedy0110 Nov 16, 2024

Choose a reason for hiding this comment

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

요거 매 호출 시점마다 OkHttpClient를 만들어줄 것 같은데요. 내부적으로 HTTP Connection 관리 등을 하고 있어서요. 매 요청마다 별도의 Client를 만들어서 제공하는 것은 비효율 적으로 보여요.

객체를 한번만 생성하고 이를 재활용 할 수 있는 방법을 고민해보심이 좋겠습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

섬세한 리뷰 감사합니다!!!!!!

Comment on lines +14 to +21
@POST("/user")
fun signUp(@Body request: SignUpRequestDto): Call<SignUpResponseDto>

@POST("/login")
fun signIn(@Body request: SignInRequestDto): Call<SignInResponseDto>

@GET("/user/my-hobby")
fun getMyHobby(): Call<GetHobbyResponseDto>

Choose a reason for hiding this comment

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

call 객체를 반환하기 보다 suspend 로 인터페이스를 변경하심을 추천드려요. (이유는 별도 코멘트)

Suggested change
@POST("/user")
fun signUp(@Body request: SignUpRequestDto): Call<SignUpResponseDto>
@POST("/login")
fun signIn(@Body request: SignInRequestDto): Call<SignInResponseDto>
@GET("/user/my-hobby")
fun getMyHobby(): Call<GetHobbyResponseDto>
@POST("/user")
suspend fun signUp(@Body request: SignUpRequestDto): SignUpResponseDto
@POST("/login")
suspend fun signIn(@Body request: SignInRequestDto): SignInResponseDto
@GET("/user/my-hobby")
suspend fun getMyHobby(): GetHobbyResponseDto

Comment on lines +62 to +98
userService.signIn(
request = SignInRequestDto(
username = signInUsername,
password = signInPassword
)
).enqueue(
object : Callback<SignInResponseDto> {
override fun onResponse(
call: Call<SignInResponseDto>,
response: Response<SignInResponseDto>
) {
if (response.isSuccessful) {
_signInResultState.value = response.body()
_signInResult.value = SignInResult.Success

response.body()?.result?.token?.let { token ->
viewModelScope.launch {
tokenManager.saveToken(token)
}
}
} else {
_signInResultState.value = response.errorBody()?.string()
?.let { Json.decodeFromString<SignInResponseDto>(it) }

if (signInResultState.value?.code == "01" && response.code() == 400) {
_signInResult.value = SignInResult.FailurePasswordLength
} else if (signInResultState.value?.code == "01" && response.code() == 403) {
_signInResult.value = SignInResult.FailureWrongPassword
}
}
}

override fun onFailure(call: Call<SignInResponseDto>, t: Throwable) {
// 어떤 구현이 들어갈까요?
}
}
)

Choose a reason for hiding this comment

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

call 사용해서 callback 형태로 비동기 요청을 처리할 때 아래와 같은 어려움이 있어요. 사실 리스트를 더 할 수도 있겠습니다.

  • signIn 호출을 한 이후에 서버에서 응답이 오기 전에 화면을 이탈하면?
  • get hobby 와 같이 응답을 사용해야 하는 요청 여럿을 동시에 하고, 이 결과를 조합하려면?

이러한 문제를 call 객체를 사용해서 해결해보시고, coroutine(suspend)를 사용할 솔루션과 비교해보시면 비동기 처리에 대한 생각을 좀 정리하실 수 있을 것 같아요.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

감사합니다 ㅠㅠ

Copy link

@hyeeum hyeeum left a comment

Choose a reason for hiding this comment

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

매주차를 거듭할수록 더욱 잘해지는 것 같은데 무섭습니다
전체적인 코드 수정도 많이 하셨고 서버통신도 하느라 시간 많이 투자하셨을 것 같은데 고생하셨습니다 : -)

)
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Routes.SignIn("", "") // 이녀석 생성자 안써서 3시간 날림
startDestination = Routes.SignIn // 이녀석 생성자 안써서 3시간 날림
Copy link

Choose a reason for hiding this comment

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

이제 불필요한 주석은 지워도 괜찮을 것 같아요!

@@ -77,7 +76,7 @@ fun WavveBottomNavigation(
@Composable
fun WavveBottomNavigationPreview() {
WavveBottomNavigation(
WavveUtils.wavveBottomNavigationItems,
listOf(),
Copy link

Choose a reason for hiding this comment

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

프리뷰에 데이터를 주지않으면 아무것도 안보이지 않을까요? 그렇다면 프리뷰의 존재의미가 없을 것 같아요 : ) !

import kotlinx.coroutines.flow.map


// token을 저장할 방법으로 공식문서가 권장하는 preferenceDatastore 채택
Copy link

Choose a reason for hiding this comment

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

좋은 정보 감사합니다! 역시 공식문서는 중요해..

Copy link

Choose a reason for hiding this comment

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

현재 컴포넌트가 비즈니스 로직을 갖고 있는 것 같은데 이러한 로직 관련은 SignInBtn이 사용되는 화면의 viewmodel에서 작성되어야 할 것 같아요 : -)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저게 비즈니스 로직인지 ui 로직인지 많이 고민했는데 역시 비즈니스로 봐야하군요 바꿔보겠쑵니다.

} else {
_signUpResultState.value = response.errorBody()?.string()
?.let { Json.decodeFromString<SignUpResponseDto>(it) }
if (signUpResultState.value?.code == "01" && response.code() == 400) {
Copy link

Choose a reason for hiding this comment

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

에러 코드의 경우 01 / 400 으로 하드코딩을 할 수도 있지만, 상수화를 시켜보는 것도 좋을 것 같아요! 물론 우리는 서버 명세서를 바로바로 확인할 수 있지만 추후 다시 볼 때 등을 고려해서 이를 가독성있게 변환해보는 건 어떨까요?

signUpPassword.any { it.isDigit() },
signUpPassword.any { !it.isLetterOrDigit() }
override fun onFailure(call: Call<SignUpResponseDto>, t: Throwable) {
// 어떤 처리를 할까요?
Copy link

Choose a reason for hiding this comment

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

저도 아직 에러처리 안하기는 했는데 ㅋㅋ ... 토스트 메세지 같이 에러 원인을 띄워주는 것도 좋을 것 같아요 ~~ 다른 분들의 의견도 궁금하네요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

지은행님도 토스트를 추천하셨씁니다

val signUpResult by signUpViewModel.signUpResult.observeAsState()
val context = LocalContext.current

LaunchedEffect(signUpResult) { // 얘는 UI 로직이라고 봐야겠죠?
Copy link

Choose a reason for hiding this comment

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

아까 위에 코멘트남겼던 Btn과 동일한 구조네요!
현재 Btn내에서 success/ Failure 등을 판단하고 있는 것 같은데 이러한 판단 구조는 컴포저블 함수인 Btn이 하기보다는 화면의 로직을 관리하는 ViewModel 에서 해주는 것이 좋아보입니다 :)

비유하자면 뭐랄까..
나는 분명 서빙 알바라고 해서 지원했는데 갑자기 식자재구매/메뉴 조리/메뉴 연구까지 시키는 느낌이에요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

볼때마다 아주 깔끔하군요 지은언니 못지않게 네이밍 센스가 넘치십니다

Copy link

@HAJIEUN02 HAJIEUN02 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!! 매주 왤케 실력이 업그레이드 돼서 오셔요?ㅜㅜ 무서워~~~

@@ -53,6 +60,15 @@ dependencies {
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.compose.navigation)
implementation(libs.androidx.runtime.livedata)

Choose a reason for hiding this comment

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

요기 아래에 한 줄만 띄워주세요 ㅎ
주석이랑 코드랑 딱 붙어있어서..

implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
implementation(libs.retrofit)

Choose a reason for hiding this comment

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

버전 카탈로그에서 bundles로 묶는 것에 대해 찾아보시면 좋을 것 같아요! 디펜던시 추가할 때 훨씬 코드가 간결해진답니다

@@ -48,4 +28,27 @@ object WavveUtils {

fun transformationPasswordVisual(isVisible: Boolean): VisualTransformation =
if (isVisible) VisualTransformation.None else PasswordVisualTransformation()

fun showToast(

Choose a reason for hiding this comment

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

미쵸따 토스트 스낵바 둘다 확장함수화
짱이네요


Spacer(modifier = Modifier.height(20.dp))
item {

Choose a reason for hiding this comment

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

scrollState를 설정해준 Column 안에 LazyRow, LazyColumn들을 선언해주는 방식에서 LazyColumn 안에 items로 LazyRow, LazyColumn들을 선언해주는 것으로 변경한 이유가 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

제대로 만들면 화면에 출력하는 아이템들이 많아져 스크롤이 길어질꺼 같은데 이때 LazyColumn의 보여지는 composable만 화면에 띄우는게 성능적으로 좋을 것 같았습니다.

@@ -33,7 +34,8 @@ fun MyInfoScreen(
.padding(paddingValues)
) {
MyInfoProfile(
myEmail = myEmail,
myInfoViewModel = myInfoViewModel,

Choose a reason for hiding this comment

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

뷰모델을 직접적으로 넘겨주는 것보다는 인자들을 분리해서 람다 등을 이용해 필요한 것들만 넘겨주시면 좋을 것 같습니다~!
이유가 뭘까용?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

첫번째로는 서로 하는 역할을 분리해놨는데 인자로 주면 종속된다(?)라는 점 일 것 같고,
두번째는 컴포즈에서 상태 호이스팅을 권장하고
세번째는 밑에서 지적받은 부분처럼 Preview시에 문제가 있을 수 있다는 점 인 것 같습니다.
정답을 알려줘~

val Context.dataStore by preferencesDataStore("token")

class TokenManager(private val context: Context) {
private val TOKEN_KEY = stringPreferencesKey("auth_token")

Choose a reason for hiding this comment

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

상수화핑

)

sealed class SignInResult {

Choose a reason for hiding this comment

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

위에서는 UiState라는 네이밍을 사용하셨는데 여기는 Result라는 네이밍을 사용하신 이유가 있을까요?
각각의 용도를 명확히 하고 같은 용도인 경우엔 네이밍을 통일해주심 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

먼가 UiState라고 하기에는 애매하긴하네요
얘가 로그인했을때 결과로 나올 수 있는 경우의 수를 모아둔 건데 다른 곳으로 빼야할까요? 어디에 두는게 좋을까요?

val signInResult: LiveData<SignInResult> = _signInResult

private val _signInResultState = mutableStateOf<SignInResponseDto?>(null)
val signInResultState: State<SignInResponseDto?> get() = _signInResultState

Choose a reason for hiding this comment

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

한 파일에 StateFlow, State, Livedata가 다 있는데 이렇게 각각 다 다르게 한 이유에 대해 알려주샤요

_signInResultState.value = response.errorBody()?.string()
?.let { Json.decodeFromString<SignInResponseDto>(it) }

if (signInResultState.value?.code == "01" && response.code() == 400) {

Choose a reason for hiding this comment

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

에러 코드 일괄적으로 상수화하면 좋을 듯!

withStyle(
style = SpanStyle(
),
start = 9,

Choose a reason for hiding this comment

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

인덱스도 상수화하면 조케다

Copy link

@1971123-seongmin 1971123-seongmin left a comment

Choose a reason for hiding this comment

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

고생하셨습니다. 항상 과제 이외에 디테일한 부분 고쳐오시는데 정말 대단하세요

Toast.LENGTH_SHORT
).show()

fun showSnackbar(

Choose a reason for hiding this comment

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

토스트만 확장함수로 만들어 뒀는데 스낵바도 추가해 봐야겠습니다.

import retrofit2.Callback
import retrofit2.Response

class MyInfoViewModel(application: Application) : AndroidViewModel(application) {

Choose a reason for hiding this comment

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

뷰모델에 Application을 넣으면 어떻게 되나요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ViewModel안에서는 context를 사용하지 못하잖아요?
AndroidViewModel은 ViewModel 서브클래스인데 context를 사용하고 싶을때 쓴다고해요.
그래서 저기 application이 어플리케이션의 context를 가지는 것 입니다.
사실 좋은 코드인지는 모르겠네요 꾸역꾸역 구현했습니다

Copy link
Contributor

Choose a reason for hiding this comment

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

Context는 뭘까요?
그리고 ViewModel이 Context를 가진다는 것,,은! 어떤 의미가 될까요? (MVVM 관점에서 생각해보세요)

import kotlinx.serialization.Serializable

@Serializable
data class GetHobbyResponseDto(

Choose a reason for hiding this comment

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

모든 서버 Response 값을 받아 감싸는 기본 BaseResponse를 만들어서 사용하시면 더 편하실 것 같아요

navigateToSignIn = {
navController.navigate(
route = Routes.SignIn,
navOptions = navOptions {

Choose a reason for hiding this comment

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

네비게이션 부분 저도 고쳐야 하는데 대단하시네요!!

import kotlinx.coroutines.flow.map


// token을 저장할 방법으로 공식문서가 권장하는 preferenceDatastore 채택

Choose a reason for hiding this comment

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

저는 그냥 데이터스토어를 썼는데 이런 방법이 있네요. 잘 배워가요

val signInResult: LiveData<SignInResult> = _signInResult

private val _signInResultState = mutableStateOf<SignInResponseDto?>(null)
val signInResultState: State<SignInResponseDto?> get() = _signInResultState

Choose a reason for hiding this comment

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

공부 목적으로 여러개 사용하신 것 같은데 하나로 통일하시면 더 좋을 것 같아요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네 다 되는지 확인하고 싶었씁니다.
stateFlow로 다 바꾸겠씁니다

Copy link
Contributor

@jihyunniiii jihyunniiii left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 ~ 합세 파이팅이에용

@@ -48,4 +28,27 @@ object WavveUtils {

fun transformationPasswordVisual(isVisible: Boolean): VisualTransformation =
if (isVisible) VisualTransformation.None else PasswordVisualTransformation()

fun showToast(
Copy link
Contributor

Choose a reason for hiding this comment

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

fun Context.showToast()의 형태로 만들어도 좋을 것 같ㄴㅔ요!
그리고 둘이 뭐가 다른지 비교해봅시다

import retrofit2.Callback
import retrofit2.Response

class MyInfoViewModel(application: Application) : AndroidViewModel(application) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Context는 뭘까요?
그리고 ViewModel이 Context를 가진다는 것,,은! 어떤 의미가 될까요? (MVVM 관점에서 생각해보세요)

Text(
text = myEmail,
Text( // 만약에 네트워크 통신이 늦었을 때 얘가 recomposition이 되나요??
text = if (myInfoUiState.myHobby.isNotEmpty()) myInfoUiState.myHobby else "Loading...",
Copy link
Contributor

Choose a reason for hiding this comment

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

우리 지은이 언제 이렇게 컸지,,

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

fun createRetrofit(context: Context): Retrofit {
Copy link
Contributor

Choose a reason for hiding this comment

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

Context를 인자로 받는 이유가 있는지 궁금합니다 !

import okhttp3.Response

class AuthInterceptor(context: Context) : Interceptor {
private val tokenManager = TokenManager(context)
Copy link
Contributor

Choose a reason for hiding this comment

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

아 이친구 때문이었군요,,! context를 인자로 받으면서 mvvm과 멀어지는 부분이 많아지는 것 같은데! 어떻게 해결하면 좋을지 고민해보면 좋을 것 같습니다! (지금 생각나는 건,,, 싱글톤 패턴 쓰기,,?)

Copy link
Contributor

Choose a reason for hiding this comment

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

몰라! 근데 아직 어려울 수 있을 것 같아서 ㅠㅠ 이번주 세미나를 듣고 고민해봐도 좋을 것 같아요!

Comment on lines +94 to +96
override fun onFailure(call: Call<SignInResponseDto>, t: Throwable) {
// 어떤 구현이 들어갈까요?
}
Copy link
Contributor

Choose a reason for hiding this comment

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

어떤 경우에 onFailure가 실행되는지를 고민해보면 어떤 로직이 들어가면 좋은지 알 수 있을 거예요! (세미나 자료에 있음)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feat] 4주차 과제
6 participants