Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ android {
buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api"].toString())
buildConfigField("String", "NAVERMAP_CLIENT_SECRET", properties["NAVERMAP_CLIENT_SECRET"].toString())
buildConfigField("String", "NAVERMAP_CLIENT_ID", properties["NAVERMAP_CLIENT_ID"].toString())
buildConfigField("String","GOOGLE_WEB_CLIENT_ID",properties["google.client.id"].toString())

manifestPlaceholders["KAKAO_NATIVE_KEY"] = properties["kakao.native.key"].toString()
}
Expand All @@ -46,6 +47,7 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
Copy link
Member

Choose a reason for hiding this comment

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

👍

}
kotlinOptions {
jvmTarget = "11"
Expand Down Expand Up @@ -108,4 +110,10 @@ dependencies {

// 네이버
implementation(libs.bundles.naverMaps)

//구글
implementation(libs.androidx.credentials)
implementation(libs.googleid)
implementation(libs.androidx.credentials.play.services.auth)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand Down Expand Up @@ -44,11 +43,10 @@ import androidx.compose.ui.zIndex
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.paw.key.R
import com.paw.key.core.designsystem.theme.PawKeyTheme
import com.paw.key.core.designsystem.theme.Gray100
import com.paw.key.core.util.noRippleClickable
import com.paw.key.core.designsystem.theme.PawKeyTheme
import com.paw.key.core.extension.noRippleClickable
import com.paw.key.domain.model.entity.walklist.CategoryTop3Entity
import kotlinx.serialization.json.JsonNull.content

@OptIn(ExperimentalLayoutApi::class)
@Composable
Expand Down
62 changes: 61 additions & 1 deletion app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ private val SELECTED_GU_NAME_KEY = stringPreferencesKey("selected_gu_name")
private val SELECTED_DONG_NAME_KEY = stringPreferencesKey("selected_dong_name")
private val ACTIVE_REGION_KEY = stringPreferencesKey("active_region")

// 토큰 관리를 위한 키들
private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")

Copy link
Member

Choose a reason for hiding this comment

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

access token과 refresh token은 평문으로 저장할 경우 보안에 문제가 생길 수 있으니 EncryptedSharedPreferences 을 사용해서 암호화해서 저장해보는 것은 어떨까 싶습니다!

Copy link
Member Author

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.

아 더 찾아보니 Tink라는 구글의 암호화 라이브러리를 사용하는 것이 더 좋아보이네요! 현재 EncryptedSharedPreferences는 보안 취약점이 있기도 했고, 개발도 중단 된 거 같네요.. Tink 관련해서는 Medium에서 확인해보시면 좋을 것 같습니다! 래퍼런스가 많이 없긴한데 편하신 걸로 구현하시면 될 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

현재 EncryptedSharedPreferences 로 작업하고 있긴했는데, Tink 로 한 번 적용해서 구현해보겠습니다. 자료감사합니당

Copy link
Member

Choose a reason for hiding this comment

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

취소.. 참조할만한 자료가 마땅치않네용 나중에 마이그레이션하던지 합시당

private fun List<LatLng>.toPreferenceString(): String =
joinToString(";") { "${it.latitude},${it.longitude}" }

Expand All @@ -46,7 +50,6 @@ object PreferenceDataStore {
private val summaryStore
get() = appContext.summaryStore

// 기존 함수들...
suspend fun saveWalkSummary(
points: List<LatLng>,
totalDistance: Float,
Expand Down Expand Up @@ -287,6 +290,63 @@ object PreferenceDataStore {
}
}

// ===== 토큰 관리 관련 함수들 =====

/**
* 액세스 토큰과 리프레시 토큰을 저장합니다
*/
suspend fun saveTokens(accessToken: String, refreshToken: String) {
summaryStore.edit { preferences ->
preferences[ACCESS_TOKEN_KEY] = accessToken
preferences[REFRESH_TOKEN_KEY] = refreshToken
}
}

/**
* 액세스 토큰을 조회합니다
*/
fun getAccessToken(): Flow<String> = summaryStore.data.map {
it[ACCESS_TOKEN_KEY] ?: ""
}

/**
* 리프레시 토큰을 조회합니다
*/
fun getRefreshToken(): Flow<String> = summaryStore.data.map {
it[REFRESH_TOKEN_KEY] ?: ""
}

/**
* 토큰 정보 데이터 클래스
*/
data class TokenInfo(
val accessToken: String,
val refreshToken: String,
) {
val isTokensAvailable: Boolean
get() = accessToken.isNotEmpty() && refreshToken.isNotEmpty()
}

/**
* 모든 토큰 정보를 한번에 조회합니다
*/
fun getTokenInfo(): Flow<TokenInfo> = summaryStore.data.map { preferences ->
TokenInfo(
accessToken = preferences[ACCESS_TOKEN_KEY] ?: "",
refreshToken = preferences[REFRESH_TOKEN_KEY] ?: ""
)
}

/**
* 토큰 정보를 초기화합니다
*/
suspend fun clearTokens() {
summaryStore.edit { preferences ->
preferences.remove(ACCESS_TOKEN_KEY)
preferences.remove(REFRESH_TOKEN_KEY)
}
}

suspend fun clearAllData() {
summaryStore.edit { it.clear() }
}
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/paw/key/core/util/suspendRunCating.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.paw.key.core.util

import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.ensureActive
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext

suspend fun <R> suspendRunCatching(block: suspend () -> R): Result<R> {
return try {
Result.success(block())
} catch (t: TimeoutCancellationException) {
Result.failure(t)
} catch (c: CancellationException) {
throw c
} catch (e: Throwable) {
coroutineContext.ensureActive()
Result.failure(e)
}
}
47 changes: 47 additions & 0 deletions app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.paw.key.data

import android.content.Context
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.paw.key.BuildConfig
import com.paw.key.core.util.suspendRunCatching
import com.paw.key.data.dto.request.LoginRequestDto
import com.paw.key.data.dto.response.BaseResponse
import com.paw.key.data.dto.response.LoginResponseDto
import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource
import com.paw.key.data.remote.datasource.login.GoogleAuthDataSource
import com.paw.key.data.service.login.LoginService
import javax.inject.Inject

class GoogleAuthDataSourceImpl @Inject constructor(
private val credentialManager: CredentialManager,
) : GoogleAuthDataSource {
override suspend fun signIn(context: Context): Result<GoogleIdTokenCredential> =
suspendRunCatching {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setAutoSelectEnabled(false)
.setServerClientId(BuildConfig.GOOGLE_WEB_CLIENT_ID)
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val response = credentialManager.getCredential(context, request)
GoogleIdTokenCredential.createFrom(response.credential.data)
}
}


class AuthRemoteDataSourceImpl @Inject constructor(
Copy link
Member

Choose a reason for hiding this comment

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

google은 google sdk 관련 책임과 역할로, AuthRemoteDataSourceImpl는 우리 백엔드와 관련된 책임과 역할이기 때문에 분리해주시면 더 좋을 것 같습니다! google sdk에서 우리 백엔드 연결 방식을 전혀 몰라야겠지요?

private val loginService: LoginService,
) : AuthRemoteDataSource {
override suspend fun login(
providerToken: String,
provider: String,
): BaseResponse<LoginResponseDto> =
loginService.login(providerToken, LoginRequestDto(provider))
}
8 changes: 8 additions & 0 deletions app/src/main/java/com/paw/key/data/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.paw.key.data.repositoryimpl.sharedwalk.SharedWalkRepositoryImpl
import com.paw.key.data.repositoryimpl.home.HomeRegionRepositoryImpl
import com.paw.key.data.repositoryimpl.home.RegionCurrentRepositoryImpl
import com.paw.key.data.repositoryimpl.list.PostsListRepositoryImpl
import com.paw.key.data.repositoryimpl.login.AuthRepositoryImpl
import com.paw.key.data.repositoryimpl.walklist.WalkListDetailRepositoryImpl
import com.paw.key.data.repositoryimpl.walkreview.WalkReviewRepositoryImpl
import com.paw.key.domain.repository.ArchivedListRepository
Expand All @@ -33,6 +34,7 @@ import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository
import com.paw.key.domain.repository.home.HomeRegionRepository
import com.paw.key.domain.repository.home.RegionCurrentRepository
import com.paw.key.domain.repository.list.PostsListRepository
import com.paw.key.domain.repository.login.AuthRepository
import com.paw.key.domain.repository.petprofile.PetProfileRepository
import com.paw.key.domain.repository.userprofile.UserProfileRepository
import com.paw.key.domain.repository.walkcourse.WalkCourseRepository
Expand Down Expand Up @@ -165,4 +167,10 @@ interface RepositoryModule {
fun bindRegionCurrentRepository(
impl: RegionCurrentRepositoryImpl
) : RegionCurrentRepository

@Binds
@Singleton
fun bindLoginRepository(
impl: AuthRepositoryImpl
) : AuthRepository
}
14 changes: 7 additions & 7 deletions app/src/main/java/com/paw/key/data/di/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import com.paw.key.data.service.ArchivedListService
import com.paw.key.data.service.DummyService
import com.paw.key.data.service.LikeService
import com.paw.key.data.service.PetProfileService
import com.paw.key.data.service.onboarding.OnboardingInfoService
import com.paw.key.data.service.onboarding.OnboardingPetsService
import com.paw.key.data.service.onboarding.OnboardingRegionService
import com.paw.key.data.service.RegionService
import com.paw.key.data.service.SavedListService
import com.paw.key.data.service.UserProfileService
import com.paw.key.data.service.filter.FilterOptionService
import com.paw.key.data.service.sharedwalk.SharedWalkService
import com.paw.key.data.service.home.HomeRegionService
import com.paw.key.data.service.home.RegionCurrentService
import com.paw.key.data.service.list.PostsListService
import com.paw.key.data.service.login.LoginService
import com.paw.key.data.service.onboarding.OnboardingInfoService
import com.paw.key.data.service.onboarding.OnboardingPetsService
import com.paw.key.data.service.onboarding.OnboardingRegionService
import com.paw.key.data.service.sharedwalk.SharedWalkService
import com.paw.key.data.service.walkcourse.WalkCourseService
import com.paw.key.data.service.walklist.WalkListDetailService
import com.paw.key.data.service.walkreview.WalkReviewService
Expand Down Expand Up @@ -68,7 +68,7 @@ object ServiceModule {
@Provides
@Singleton
fun provideHomeRegionService(retrofit: Retrofit): HomeRegionService =
retrofit.create(HomeRegionService::class.java)
retrofit.create()

//마이페이지
@Provides
Expand Down Expand Up @@ -119,6 +119,6 @@ object ServiceModule {

@Provides
@Singleton
fun provideRegionCurrentService(retrofit: Retrofit): RegionCurrentService =
fun provideLoginService(retrofit: Retrofit): LoginService =
retrofit.create()
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.paw.key.data.dto.request

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

@Serializable
data class LoginRequestDto (
@SerialName("email")
val email: String,
)
// 테스트용입니다
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.paw.key.data.dto.response

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

@Serializable
data class LoginResponseDto (
@SerialName("AccessToken")
val AccessToken: String,
@SerialName("RefreshToken")
val RefreshToken: String
)
// 테스트용입니다
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.paw.key.data.remote.datasource.home

import com.paw.key.data.dto.request.home.HomeRegionRequest
import com.paw.key.data.service.home.HomeRegionService
import com.paw.key.data.service.home.RegionCurrentService
import javax.inject.Inject

class RegionCurrentDataSource @Inject constructor(
private val service: RegionCurrentService
private val service: HomeRegionService
) {
suspend fun RegionCurrent(userId: Int) =
service.RegionCurrent(userId)
suspend fun regionCurrent(userId: Int) =
service.regionCurrent(userId)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.paw.key.data.remote.datasource.login

import android.content.Context
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.paw.key.data.dto.response.BaseResponse
import com.paw.key.data.dto.response.LoginResponseDto

interface GoogleAuthDataSource {
suspend fun signIn(context: Context): Result<GoogleIdTokenCredential>
}

interface AuthRemoteDataSource {
suspend fun login(providerToken: String, provider: String): BaseResponse<LoginResponseDto>
}
Copy link
Member

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
Expand Up @@ -9,9 +9,9 @@ class RegionCurrentRepositoryImpl @Inject constructor(
private val dataSource: RegionCurrentDataSource,
) : RegionCurrentRepository {

override suspend fun RegionCurrent(userId: Int): Result<RegionCurrentDataEntity> {
override suspend fun regionCurrent(userId: Int): Result<RegionCurrentDataEntity> {
return runCatching {
val response = dataSource.RegionCurrent(userId)
val response = dataSource.regionCurrent(userId)
if (response.code == "S000") {
Copy link
Member

Choose a reason for hiding this comment

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

어허 response.code 금지, 코드로 확인하지말고 result의 onSuccess나 onFailure로 판단합시다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아 맞다

// 올바른 타입 반환 (RegionCurrentDataEntity)
response.data.toEntity() // DTO에서 Entity로 변환
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.paw.key.data.repositoryimpl.login

import android.content.Context
import com.paw.key.core.util.PreferenceDataStore
import com.paw.key.core.util.suspendRunCatching
import com.paw.key.data.dto.response.LoginResponseDto
import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource
import com.paw.key.data.remote.datasource.login.GoogleAuthDataSource
import com.paw.key.domain.repository.login.AuthRepository
import javax.inject.Inject

class AuthRepositoryImpl @Inject constructor(
private val authRemoteDataSource: AuthRemoteDataSource,
private val googleAuthDataSource: GoogleAuthDataSource,
) : AuthRepository {
override suspend fun signInWithGoogle(context: Context): Result<String> =
googleAuthDataSource.signIn(context).map { it.idToken }

override suspend fun login(providerToken: String, provider: String): Result<LoginResponseDto> =
suspendRunCatching {
val loginResponse = authRemoteDataSource.login(providerToken, provider).data
PreferenceDataStore.saveTokens(
accessToken = loginResponse.AccessToken,
refreshToken = loginResponse.RefreshToken
)
loginResponse
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.paw.key.data.service.home

import com.paw.key.data.dto.request.home.HomeRegionRequest
import com.paw.key.data.dto.response.BaseResponse
import com.paw.key.data.dto.response.home.HomeRegionResponse
import com.paw.key.data.dto.response.home.RegionCurrentResponseDto
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.PATCH

Expand All @@ -14,4 +17,8 @@ interface HomeRegionService {
@Body request: HomeRegionRequest,
): HomeRegionResponse<Unit?>

@GET("regions/current")
suspend fun regionCurrent(
@Header("X-USER-ID") userId: Int
): BaseResponse<RegionCurrentResponseDto>
}
Loading