Skip to content

Commit

Permalink
Merge pull request #4 from AND-SOPT-ANDROID/week4
Browse files Browse the repository at this point in the history
4주차 과제
  • Loading branch information
ThirFir authored Dec 6, 2024
2 parents 1e91fba + 53a1f6e commit e67afb9
Show file tree
Hide file tree
Showing 57 changed files with 1,278 additions and 322 deletions.
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"
}

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"]}\")")
}

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 }
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
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
)
}
}
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)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).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()
}
}

@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"
}
}
Loading

0 comments on commit e67afb9

Please sign in to comment.