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

4주차 과제 #4

Merged
merged 41 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
45f5c41
add: Retrofit dependency
ThirFir Nov 2, 2024
9bb8c16
add: 회원가입 API 연동
ThirFir Nov 5, 2024
9c4c8e6
add: hobby 입력 필드 추가
ThirFir Nov 5, 2024
6d30f25
fix: email -> username
ThirFir Nov 5, 2024
570fc44
fix: Password, hobby validation 검사 변경
ThirFir Nov 5, 2024
456cb7f
add: 로그인 API 연동
ThirFir Nov 5, 2024
166364c
add: result unwrap call adapter factory
ThirFir Nov 6, 2024
c38fd5f
fix: suspend
ThirFir Nov 6, 2024
6cf6f0e
add: EncryptedSharedPref dependency
ThirFir Nov 6, 2024
df71590
add: 회원가입, 로그인 응답 객체 설정 및 토큰 저장
ThirFir Nov 6, 2024
1bdaa01
refactor: User -> TokenLocalDataSource 변경 및 token 헤더 추가
ThirFir Nov 6, 2024
3f79f7a
fix: 인터셉터 제거, 내 취미 api 연동
ThirFir Nov 6, 2024
62c4df2
add: 내 취미 api 호출 로직
ThirFir Nov 6, 2024
49e7937
add: 내 취미 뷰에 추가
ThirFir Nov 6, 2024
15caab8
add: Success Response Converter
ThirFir Nov 11, 2024
90e1ea6
add: Error response parsing
ThirFir Nov 11, 2024
d787e18
fix: runCatching 잘못 사용
ThirFir Nov 11, 2024
1279cf8
add: runCatching 확장함수
ThirFir Nov 11, 2024
d099448
refactor: runCatching -> 커스텀 확장함수
ThirFir Nov 11, 2024
834c5ab
add: 취미 입력
ThirFir Nov 11, 2024
392f586
refactor: 회원가입 메시지 변경
ThirFir Nov 11, 2024
19f90f1
fix: runCatching 중복
ThirFir Nov 11, 2024
0c1c9a6
add: 로그인 API 결과
ThirFir Nov 11, 2024
05f69b1
add: 로그인 성공 시 키보드 hide
ThirFir Nov 11, 2024
743bb86
chore: get -> fetch
ThirFir Nov 11, 2024
c37bf31
add: 다른사람 취미 조회 API
ThirFir Nov 11, 2024
c25a680
add: 다른사람 취미 조회 로직 및 뷰
ThirFir Nov 11, 2024
0aff239
add: 내 설정 route
ThirFir Nov 11, 2024
dc2a5e5
add: 네비게이션 연결
ThirFir Nov 11, 2024
9550255
add: domain coroutine dependency
ThirFir Nov 11, 2024
a583091
add: 프로필 수정 API
ThirFir Nov 11, 2024
243cc60
add: 디스패처 DI
ThirFir Nov 11, 2024
f2d7cbd
add: EditProfileScreen
ThirFir Nov 11, 2024
ffd39f2
refactor: 취미 fetching 로직
ThirFir Nov 11, 2024
c6f0a26
fix: navigation 애니메이션 제거(임시)
ThirFir Nov 11, 2024
8afb1dc
refactor: Debounce search로 변경
ThirFir Nov 12, 2024
2ccae19
add: 자동 로그인
ThirFir Nov 12, 2024
902a06e
chore: 변수명 명료화
ThirFir Nov 12, 2024
d2ec5b0
chore: 리뷰 반영
ThirFir Nov 12, 2024
29c9025
add: 검색 로딩 뷰
ThirFir Nov 14, 2024
53a1f6e
chore: 로깅 제거
ThirFir Nov 14, 2024
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
21 changes: 21 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 추가할 수도 있군요!
위 코드 처럼 alias(libs.plugins.kotlin.serialization) 이런식으로 추가해주는게 어떨까요..?!

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 localProperties = Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}

android {
Expand All @@ -18,6 +25,7 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", "String.valueOf(\"${localProperties["base_url"]}\")")
Copy link
Member

Choose a reason for hiding this comment

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

"String.valueOf("${localProperties["base_url"]}")" => localProperties["base_url"].toString() 로 작성하는건 어떨까요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

local.properties에 키 작성할 때 base_url="qwerasdf1234" 이런식으로 따옴표를 붙일 수 있지만, 저는 따옴표를 붙이지않고 base_url=qwerasdf1234 이렇게 쓰고 싶었습니다 !
그런데 따옴표를 붙이면 toString()으로 되는데, 따옴표를 붙이지 않으면 toString으로 되진 않고 저 방식으로만 해야 했습니다 !!

Copy link
Contributor

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.

manifestPlaceholders로 Manifest에 변수 넣을 때는 따옴표를 넣으면 안되더라구여. 그래서 저는 통일성을 위해 전부 따옴표 없이 사용합니당
(지금은 상관없긴하지만..!)

}

buildTypes {
Expand All @@ -36,6 +44,9 @@ android {
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
buildConfig = true
}
}

kapt {
Expand Down Expand Up @@ -66,4 +77,14 @@ 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)

implementation(libs.androidx.security.crypto.ktx)

}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ANDANDROID"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name="com.sopt.presentation.MainActivity"
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/org/sopt/and/adapter/ResponseConverter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.sopt.and.adapter

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.Type

class ResultConverter<T>(
private val serializer: KSerializer<T>,
private val json: Json
) : Converter<ResponseBody, T> {
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)
}
}
}

class ResultConverterFactory : Converter.Factory() {

override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val json = Json { ignoreUnknownKeys = true }

Choose a reason for hiding this comment

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

coerceInputValues, encodeDefaults, isLenient 등등 option 값들이 정말 많습니다! 해당 옵션들이 정말 필요한 경우가 많기 때문에 한 번 알아보시면 좋을 것 같아요!

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 resultSerializer = json.serializersModule.serializer(type)
return ResultConverter(resultSerializer, json)
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/org/sopt/and/di/ApiModule.kt
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.

저도 사실 과제를 통해서 처음해보는 것들이 대부분입니다...!

새로운 코드에 너무 부담 안느끼고 막 다 해보는 게 좋은 것 같아요 !!

Original file line number Diff line number Diff line change
@@ -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)
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/org/sopt/and/di/CoroutineModule.kt
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 24 additions & 1 deletion app/src/main/java/org/sopt/and/di/LocalApiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.TokenEncryptedSharedPref
import com.sopt.data.UserSharedPref
import dagger.Module
import dagger.Provides
Expand All @@ -17,9 +20,29 @@ object LocalApiModule {
@UserSharedPref
@Provides
@Singleton
fun provideUserLocalDataSource(
fun provideUserSharedPreferences(
@ApplicationContext context: Context
): SharedPreferences {
return context.getSharedPreferences("user.pref", Context.MODE_PRIVATE)
}

@TokenEncryptedSharedPref
@Provides
@Singleton
fun provideTokenEncryptedSharedPreferences(
@ApplicationContext context: Context
): SharedPreferences {
val masterKeyAlias = MasterKey
.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
"token.pref.enc",
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
Comment on lines +23 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.

오 좋네요. 암호화된 SharedPreferences 저도 다음에 한번 써봐야겠군요..

}
82 changes: 82 additions & 0 deletions app/src/main/java/org/sopt/and/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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.ResultConverterFactory
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Singleton
@Provides
fun provideClient(
responseInterceptor: Interceptor
): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
Comment on lines +32 to +34

Choose a reason for hiding this comment

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

해당 �timeout 이 각각 10, 30, 15인 이유가 있을까요?? 지금은 상관 없지만 파일 같은걸 업로드 하는 데에 writeTimeout 이 15초로 부족할 수도 있긴합니다! (물론 그 때는 업로드용 OkhttpClient를 timeOut이 널널하게 만들면 좋겠죠!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

딱히 큰 의미를 둔 건 아닙니다 ㅎㅎ..
이것도 적정 시간 같은 게 있는지 더 알아봐야 할 것 같습니다 🫠

.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY

Choose a reason for hiding this comment

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

level 은 항상 BODY 를 출력하는게 좋진 않을 것 같아요! 개발을 하는 동안 DEBUG 단계에서만 출력 되어도 좋을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BuildConfig로 구분하겠습니다 !

}).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<ErrorResponse>(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)
.client(client)
.addConverterFactory(ResultConverterFactory())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
}
Comment on lines +64 to +77
Copy link
Member

Choose a reason for hiding this comment

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

val json = Json { ignoreUnknownKeys = true } 이거 어떤이유로 사용한건지 여쭤봐도 될까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

데이터 클래스로 지정해두지 않은 필드가 API 통신을 통해 들어왔을 때, 그 필드를 무시하는 속성입니다 !
이번 과제에선 큰 의미는 없는데, 그냥 써봤습니다 ㅎㅎ


@Serializable
private data class ErrorResponse(
val code: String
)
8 changes: 8 additions & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.hilt)
kotlin("plugin.serialization") version "2.0.20"
}

android {
Expand Down Expand Up @@ -51,4 +52,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)
}
10 changes: 9 additions & 1 deletion data/src/main/java/com/sopt/data/Qualifiers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class UserSharedPref
annotation class UserSharedPref

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TokenEncryptedSharedPref

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IODispatcher
35 changes: 35 additions & 0 deletions data/src/main/java/com/sopt/data/api/remote/UserApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 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 {

@POST("user")
suspend fun signUp(@Body signUpRequest: SignUpRequest): SignUpDto

@POST("login")
suspend fun signIn(@Body signInRequest: SignInRequest): SignInDto

@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

@PUT("user")
suspend fun updateProfile(
@Header("token") token: String,
@Body updateProfileRequest: UpdateProfileRequest
)
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Comment on lines +7 to +24
Copy link

Choose a reason for hiding this comment

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

최고다

Copy link
Member

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
Contributor Author

@ThirFir ThirFir Nov 17, 2024

Choose a reason for hiding this comment

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

네?
감사합니다.

Loading