From 8929711f99e40130348632d4b18ad153f983753a Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:50:54 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feature/#135:=20util=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/paw/key/core/designsystem/component/CourseDetail.kt | 6 ++---- .../course/entire/tab/map/List/CourseOptionBottomSheet.kt | 2 +- .../paw/key/presentation/ui/home/component/TrackingCard.kt | 1 + .../com/paw/key/presentation/ui/signup/SignUpDogScreen.kt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt index 9736d201..a68fa864 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt @@ -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 @@ -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 diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt index 291c0202..7510e675 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt @@ -40,7 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.course.entire.tab.map.List.state.TapListContract import com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel.TapListViewModel diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt b/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt index 717a8f24..df2cbca6 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt @@ -38,6 +38,7 @@ private fun PreviewTrackingCard() { @Composable fun TrackingCard( onClick: () -> Unit, + modifier: Modifier = Modifier ) { Box( contentAlignment = Alignment.Center, diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt index cdefe00d..92a0b3a4 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt @@ -57,7 +57,7 @@ import coil.compose.AsyncImage import com.paw.key.R import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.signup.component.FormField import com.paw.key.presentation.ui.signup.component.SignUpTextField import com.paw.key.presentation.ui.signup.component.SignUpUserSelectButton From 27348a82e66fce4e7fe723dfb49af5e270eb62c8 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:51:24 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feature/#135:=20google=20login=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/paw/key/core/util/suspendRunCating.kt | 19 ++++++++ .../paw/key/data/GoogleAuthDataSourceImpl.kt | 47 +++++++++++++++++++ .../key/data/dto/request/LoginRequestDto.kt | 11 +++++ .../key/data/dto/response/LoginResponseDto.kt | 13 +++++ .../datasource/login/GoogleAuthDataSource.kt | 14 ++++++ .../login/AuthRepositoryImpl.kt | 28 +++++++++++ .../key/data/service/login/LoginService.kt | 17 +++++++ .../domain/model/entity/login/LoginModel.kt | 6 +++ .../domain/repository/login/AuthRepository.kt | 9 ++++ 9 files changed, 164 insertions(+) create mode 100644 app/src/main/java/com/paw/key/core/util/suspendRunCating.kt create mode 100644 app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/service/login/LoginService.kt create mode 100644 app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/login/AuthRepository.kt diff --git a/app/src/main/java/com/paw/key/core/util/suspendRunCating.kt b/app/src/main/java/com/paw/key/core/util/suspendRunCating.kt new file mode 100644 index 00000000..8e149871 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/util/suspendRunCating.kt @@ -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 suspendRunCatching(block: suspend () -> R): Result { + return try { + Result.success(block()) + } catch (t: TimeoutCancellationException) { + Result.failure(t) + } catch (c: CancellationException) { + throw c + } catch (e: Throwable) { + coroutineContext.ensureActive() + Result.failure(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt b/app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt new file mode 100644 index 00000000..3924259f --- /dev/null +++ b/app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt @@ -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 = + 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( + private val loginService: LoginService, +) : AuthRemoteDataSource { + override suspend fun login( + providerToken: String, + provider: String, + ): BaseResponse = + loginService.login(providerToken, LoginRequestDto(provider)) +} diff --git a/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt new file mode 100644 index 00000000..dc94725c --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt @@ -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, +) +// 테스트용입니다 \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt new file mode 100644 index 00000000..c0b82bd2 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt @@ -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 +) +// 테스트용입니다 \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt new file mode 100644 index 00000000..8e7edb55 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt @@ -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 +} + +interface AuthRemoteDataSource { + suspend fun login(providerToken: String, provider: String): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt new file mode 100644 index 00000000..ccb7bb72 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt @@ -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 = + googleAuthDataSource.signIn(context).map { it.idToken } + + override suspend fun login(providerToken: String, provider: String): Result = + suspendRunCatching { + val loginResponse = authRemoteDataSource.login(providerToken, provider).data + PreferenceDataStore.saveTokens( + accessToken = loginResponse.AccessToken, + refreshToken = loginResponse.RefreshToken + ) + loginResponse + } +} diff --git a/app/src/main/java/com/paw/key/data/service/login/LoginService.kt b/app/src/main/java/com/paw/key/data/service/login/LoginService.kt new file mode 100644 index 00000000..020c63ab --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/login/LoginService.kt @@ -0,0 +1,17 @@ +package com.paw.key.data.service.login + +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 retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + + +interface LoginService { + @POST("api/v1/auth/login") + suspend fun login( + @Header("Authorization") providerToken: String, + @Body loginRequestDto: LoginRequestDto + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt b/app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt new file mode 100644 index 00000000..90f23c22 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt @@ -0,0 +1,6 @@ +package com.paw.key.domain.model.entity.login + +data class LoginModel ( + val AccessToken: String, + val RefreshToken: String +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/login/AuthRepository.kt b/app/src/main/java/com/paw/key/domain/repository/login/AuthRepository.kt new file mode 100644 index 00000000..c0d7079a --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/login/AuthRepository.kt @@ -0,0 +1,9 @@ +package com.paw.key.domain.repository.login + +import android.content.Context +import com.paw.key.data.dto.response.LoginResponseDto + +interface AuthRepository { + suspend fun signInWithGoogle(context: Context): Result + suspend fun login(providerToken: String, provider: String): Result +} \ No newline at end of file From 0ad351eb61978bea5894d49f80336f49b7ddf332 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:51:56 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feature/#135:=20home=20service=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/service/home/HomeRegionService.kt | 7 ++ .../key/presentation/ui/home/HomeScreen.kt | 1 - .../ui/home/state/HomeContract.kt | 3 - .../ui/home/viewmodel/HomeViewModel.kt | 97 ++----------------- 4 files changed, 14 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/paw/key/data/service/home/HomeRegionService.kt b/app/src/main/java/com/paw/key/data/service/home/HomeRegionService.kt index b53ec1fc..4276b144 100644 --- a/app/src/main/java/com/paw/key/data/service/home/HomeRegionService.kt +++ b/app/src/main/java/com/paw/key/data/service/home/HomeRegionService.kt @@ -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 @@ -14,4 +17,8 @@ interface HomeRegionService { @Body request: HomeRegionRequest, ): HomeRegionResponse + @GET("regions/current") + suspend fun regionCurrent( + @Header("X-USER-ID") userId: Int + ): BaseResponse } diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt index fe81fe91..f7734221 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt @@ -95,7 +95,6 @@ fun HomeScreen( val view = LocalView.current val window = (view.context as? Activity)?.window val postsResult = state.postsResult - val posts = postsResult?.posts SideEffect { window?.let { diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt b/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt index 7a3d8953..b2f934de 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt @@ -56,9 +56,6 @@ class HomeContract { } } } - - val isLocationSelected: Boolean - get() = selectedGuId != 0 && selectedDongId != 0 } @Immutable diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt index 17371a9a..46626b40 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt @@ -15,12 +15,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val regionRepository: OnboardingRegionRepository, - private val regionCurrentRepository: RegionCurrentRepository + private val regionCurrentRepository: RegionCurrentRepository, ) : ViewModel() { private val _state = MutableStateFlow(HomeContract.HomeState()) @@ -33,51 +34,7 @@ class HomeViewModel @Inject constructor( init { fetchRegion() -// loadSavedLocationInfo() - regionCurrent() // 현재 지역 정보 가져오기 추가 - } - - /** - * 저장된 위치 정보를 불러와서 상태에 반영 - */ - private fun loadSavedLocationInfo() { - viewModelScope.launch { - try { - // LocationInfo와 ActiveRegion을 모두 가져오기 - val locationInfo = PreferenceDataStore.getLocationInfo().first() - val activeRegion = PreferenceDataStore.getActiveRegion().first() - - Log.d("HomeViewModel", "저장된 위치 정보 불러오기:") - Log.d("HomeViewModel", " - 구: ${locationInfo.guName} (ID: ${locationInfo.guId})") - Log.d("HomeViewModel", " - 동: ${locationInfo.dongName} (ID: ${locationInfo.dongId})") - Log.d("HomeViewModel", " - 활동지역: $activeRegion") - - _state.update { currentState -> - currentState.copy( - selectedLocation = HomeContract.LocationInfo( - selectedGuId = locationInfo.guId, - selectedDongId = locationInfo.dongId, - selectedGu = locationInfo.guName, - selectedDong = locationInfo.dongName - ) - ) - } - - // 위치 정보가 있으면 표시용 로그 - val displayLocation = if (locationInfo.guName.isNotEmpty() && locationInfo.dongName.isNotEmpty()) { - "${locationInfo.guName} ${locationInfo.dongName}" - } else if (activeRegion.isNotEmpty()) { - activeRegion - } else { - "위치를 선택해주세요" - } - - Log.d("HomeViewModel", "TopBar 표시 위치: $displayLocation") - - } catch (e: Exception) { - Log.e("HomeViewModel", "저장된 위치 정보 불러오기 실패: ${e.message}") - } - } + regionCurrent() } fun toggleLocationMenu() { @@ -108,7 +65,6 @@ class HomeViewModel @Inject constructor( isLocationMenuVisible = false ) } - Log.d("HomeViewModel", "구 선택 완료: $guName (ID: $guId)") } catch (e: Exception) { Log.e("HomeViewModel", "구 선택 저장 실패: ${e.message}") @@ -137,12 +93,11 @@ class HomeViewModel @Inject constructor( ) ) } - - Log.d("HomeViewModel", "동 선택 완료: $dongName (ID: $dongId)") - Log.d("HomeViewModel", "전체 위치: ${currentLocation.selectedGu} $dongName") + Timber.d("HomeViewmodel", "동 선택 완료: $dongName (ID: $dongId)") } catch (e: Exception) { - Log.e("HomeViewModel", "동 선택 저장 실패: ${e.message}") + Timber.e("HomeViewmodel", "동 선택 저장 실패: ${e.message}") } + } } @@ -151,7 +106,7 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { try { - val result = regionCurrentRepository.RegionCurrent(userId.first()) + val result = regionCurrentRepository.regionCurrent(userId.first()) result.onSuccess { response -> Log.d("HomeViewModel", "RegionCurrent 성공: ${response.fullRegionName}") PreferenceDataStore.saveActiveRegion(response.fullRegionName) @@ -256,42 +211,4 @@ class HomeViewModel @Inject constructor( } } } - - fun clearError() { - _state.update { - it.copy( - uiState = it.uiState.copy(error = null) - ) - } - } - - fun refreshPosts() { - _state.update { it.copy(uiState = it.uiState.copy(isLoading = true)) } - - viewModelScope.launch { - try { - // 여기에 실제 포스트 데이터를 가져오는 로직 추가 - // val result = postsRepository.getPosts(...) - - _state.update { - it.copy( - uiState = it.uiState.copy( - isLoading = false, - error = null - ) - ) - } - } catch (e: Exception) { - Log.e("HomeViewModel", "refreshPosts Exception: ${e.message}") - _state.update { - it.copy( - uiState = it.uiState.copy( - isLoading = false, - error = e.message - ) - ) - } - } - } - } } \ No newline at end of file From f71faa3bf06234e42ddce548d488893eeddda7f9 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:52:26 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feature/#135:=20google=20login=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paw/key/core/util/PreferenceDataStore.kt | 62 ++++++++++++++++++- .../ui/login/navigation/LoginNavigation.kt | 2 + .../ui/login/viewmodel/LoginViewModel.kt | 14 ++--- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt index 843e3b9f..ffe6d73c 100644 --- a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt +++ b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt @@ -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") + private fun List.toPreferenceString(): String = joinToString(";") { "${it.latitude},${it.longitude}" } @@ -46,7 +50,6 @@ object PreferenceDataStore { private val summaryStore get() = appContext.summaryStore - // 기존 함수들... suspend fun saveWalkSummary( points: List, totalDistance: Float, @@ -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 = summaryStore.data.map { + it[ACCESS_TOKEN_KEY] ?: "" + } + + /** + * 리프레시 토큰을 조회합니다 + */ + fun getRefreshToken(): Flow = 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 = 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() } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt index 252d31df..9b7df2e8 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt @@ -23,6 +23,7 @@ fun NavGraphBuilder.loginNavGraph( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateNext: () -> Unit, + navigateHome: () -> Unit, snackBarHostState: SnackbarHostState ) { composable { @@ -30,6 +31,7 @@ fun NavGraphBuilder.loginNavGraph( paddingValues = paddingValues, navigateUp = navigateUp, navigateNext = navigateNext, + navigateHome = navigateHome, snackBarHostState = snackBarHostState ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt index eb82ebf6..a05c8a49 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt @@ -2,6 +2,7 @@ package com.paw.key.presentation.ui.login.viewmodel import androidx.lifecycle.ViewModel import androidx.navigation.NavController +import com.paw.key.domain.repository.login.AuthRepository import com.paw.key.presentation.ui.login.state.LoginContract import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -9,14 +10,14 @@ import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject class LoginViewModel @Inject constructor( - + private val authRepository: AuthRepository, ) : ViewModel() { private val _state = MutableStateFlow(LoginContract.LoginState()) - val state : StateFlow + val state: StateFlow get() = _state.asStateFlow() private val _sideEffect = MutableStateFlow(null) - val sideEffect : StateFlow + val sideEffect: StateFlow get() = _sideEffect.asStateFlow() fun onEmailChanged(email: String) { @@ -37,11 +38,4 @@ class LoginViewModel @Inject constructor( ) } - - fun onClickSignUp(navController: NavController, email: String, password: String) { - val encodedEmail = java.net.URLEncoder.encode(email, "UTF-8") - val encodedPassword = java.net.URLEncoder.encode(password, "UTF-8") - navController.navigate("signup/$encodedEmail/$encodedPassword") - } - } \ No newline at end of file From 658f4c10845ba923399df4f277d4014c8f78d25d Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:52:44 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feature/#135:=20home=20service=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/paw/key/data/di/RepositoryModule.kt | 8 ++++++++ .../main/java/com/paw/key/data/di/ServiceModule.kt | 14 +++++++------- .../datasource/home/RegionCurrentDataSource.kt | 8 +++----- .../home/RegionCurrentRepositoryImpl.kt | 4 ++-- .../key/data/service/home/RegionCurrentService.kt | 13 ------------- .../repository/home/RegionCurrentRepository.kt | 3 +-- 6 files changed, 21 insertions(+), 29 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/data/service/home/RegionCurrentService.kt diff --git a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt index 42735e1f..f49cf7eb 100644 --- a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt @@ -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 @@ -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 @@ -165,4 +167,10 @@ interface RepositoryModule { fun bindRegionCurrentRepository( impl: RegionCurrentRepositoryImpl ) : RegionCurrentRepository + + @Binds + @Singleton + fun bindLoginRepository( + impl: AuthRepositoryImpl + ) : AuthRepository } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt index eecd92db..8d5f9f23 100644 --- a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt +++ b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt @@ -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 @@ -68,7 +68,7 @@ object ServiceModule { @Provides @Singleton fun provideHomeRegionService(retrofit: Retrofit): HomeRegionService = - retrofit.create(HomeRegionService::class.java) + retrofit.create() //마이페이지 @Provides @@ -119,6 +119,6 @@ object ServiceModule { @Provides @Singleton - fun provideRegionCurrentService(retrofit: Retrofit): RegionCurrentService = + fun provideLoginService(retrofit: Retrofit): LoginService = retrofit.create() } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/home/RegionCurrentDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/home/RegionCurrentDataSource.kt index 573f7408..a07b9a3f 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/home/RegionCurrentDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/home/RegionCurrentDataSource.kt @@ -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) } diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt index 81035d5a..b738422f 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt @@ -9,9 +9,9 @@ class RegionCurrentRepositoryImpl @Inject constructor( private val dataSource: RegionCurrentDataSource, ) : RegionCurrentRepository { - override suspend fun RegionCurrent(userId: Int): Result { + override suspend fun regionCurrent(userId: Int): Result { return runCatching { - val response = dataSource.RegionCurrent(userId) + val response = dataSource.regionCurrent(userId) if (response.code == "S000") { // 올바른 타입 반환 (RegionCurrentDataEntity) response.data.toEntity() // DTO에서 Entity로 변환 diff --git a/app/src/main/java/com/paw/key/data/service/home/RegionCurrentService.kt b/app/src/main/java/com/paw/key/data/service/home/RegionCurrentService.kt deleted file mode 100644 index a76e8294..00000000 --- a/app/src/main/java/com/paw/key/data/service/home/RegionCurrentService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.paw.key.data.service.home - -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.home.RegionCurrentResponseDto -import retrofit2.http.GET -import retrofit2.http.Header - -interface RegionCurrentService { - @GET("regions/current") - suspend fun RegionCurrent( - @Header("X-USER-ID") userId: Int - ):BaseResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt b/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt index db5c32d2..7c999e84 100644 --- a/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt @@ -1,8 +1,7 @@ package com.paw.key.domain.repository.home -import com.paw.key.domain.model.entity.home.HomeRegionDataEntity import com.paw.key.domain.model.entity.home.RegionCurrentDataEntity interface RegionCurrentRepository { - suspend fun RegionCurrent(userId: Int): Result + suspend fun regionCurrent(userId: Int): Result } \ No newline at end of file From c2f6470cb3b54ade80414c9b5e761691e2b96e78 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Wed, 17 Sep 2025 20:52:57 +0900 Subject: [PATCH 06/11] feature/#135: google login version --- app/build.gradle.kts | 8 ++++++++ gradle/libs.versions.toml | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4ff3eb0e..e17055ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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() } @@ -46,6 +47,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { jvmTarget = "11" @@ -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) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 810d2f11..9ea76049 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,11 @@ timber = "5.0.1" kakaoMaps = "2.9.5" v2All = "2.20.1" +# Google +credentials = "1.6.0-alpha03" +googleid = "1.1.1" +desugar = "2.1.3" + # ServiceLocation playServicesLocation = "21.3.0" @@ -128,6 +133,12 @@ naver-map-compose = { group = "io.github.fornewid", name = "naver-map-compose", naver-map-location = { group = "io.github.fornewid", name = "naver-map-location", version.ref = "naverMapLocation" } naver-map-sdk = { group = "com.naver.maps", name = "map-sdk", version.ref = "naverMapSdk" } +# Google +androidx-credentials = { module = "androidx.credentials:credentials", version.ref = "credentials" } +androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentials" } +googleid = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleid" } +desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From ac96d9178c51325787381d1c1c1a9a63d2cb49d1 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Mon, 22 Sep 2025 18:59:33 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feature/#135:=20google=20credentials=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ea76049..1aa8c176 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ kakaoMaps = "2.9.5" v2All = "2.20.1" # Google -credentials = "1.6.0-alpha03" +credentials = "1.5.0" googleid = "1.1.1" desugar = "2.1.3" From 06e30d10cb720009cad1f61834792ac57630bfff Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Mon, 22 Sep 2025 19:00:01 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feature/#135:=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/data/dto/request/LoginRequestDto.kt | 8 ++++- .../AuthRemoteDataSourceImpl.kt | 18 +++++++++++ .../GoogleAuthDataSourceImpl.kt | 13 +------- .../datasource/login/AuthRemoteDataSource.kt | 8 +++++ .../datasource/login/GoogleAuthDataSource.kt | 4 --- .../home/RegionCurrentRepositoryImpl.kt | 3 +- .../login/AuthRepositoryImpl.kt | 17 +++++++---- .../ui/login/state/LoginContract.kt | 30 +++++++++---------- 8 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/AuthRemoteDataSourceImpl.kt rename app/src/main/java/com/paw/key/data/{ => remote/datasource/datasourceimpl}/GoogleAuthDataSourceImpl.kt (81%) create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/login/AuthRemoteDataSource.kt diff --git a/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt index dc94725c..7e780431 100644 --- a/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/request/LoginRequestDto.kt @@ -8,4 +8,10 @@ data class LoginRequestDto ( @SerialName("email") val email: String, ) -// 테스트용입니다 \ No newline at end of file +// 테스트용입니다 + + +fun LoginRequestDto.toEntity(): LoginRequestDto { + val email = this.email + return LoginRequestDto(email) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/AuthRemoteDataSourceImpl.kt b/app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/AuthRemoteDataSourceImpl.kt new file mode 100644 index 00000000..ba3bbd12 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/AuthRemoteDataSourceImpl.kt @@ -0,0 +1,18 @@ +package com.paw.key.data.remote.datasource.datasourceimpl + +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.service.login.LoginService +import javax.inject.Inject + +class AuthRemoteDataSourceImpl @Inject constructor( + private val loginService: LoginService, +) : AuthRemoteDataSource { + override suspend fun login( + providerToken: String, + provider: String, + ): BaseResponse = + loginService.login(providerToken, LoginRequestDto(provider)) +} diff --git a/app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt b/app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/GoogleAuthDataSourceImpl.kt similarity index 81% rename from app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt rename to app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/GoogleAuthDataSourceImpl.kt index 3924259f..7bfa8b0d 100644 --- a/app/src/main/java/com/paw/key/data/GoogleAuthDataSourceImpl.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/datasourceimpl/GoogleAuthDataSourceImpl.kt @@ -1,4 +1,4 @@ -package com.paw.key.data +package com.paw.key.data.remote.datasource.datasourceimpl import android.content.Context import androidx.credentials.CredentialManager @@ -34,14 +34,3 @@ class GoogleAuthDataSourceImpl @Inject constructor( GoogleIdTokenCredential.createFrom(response.credential.data) } } - - -class AuthRemoteDataSourceImpl @Inject constructor( - private val loginService: LoginService, -) : AuthRemoteDataSource { - override suspend fun login( - providerToken: String, - provider: String, - ): BaseResponse = - loginService.login(providerToken, LoginRequestDto(provider)) -} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/login/AuthRemoteDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/login/AuthRemoteDataSource.kt new file mode 100644 index 00000000..d258304d --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/login/AuthRemoteDataSource.kt @@ -0,0 +1,8 @@ +package com.paw.key.data.remote.datasource.login + +import com.paw.key.data.dto.response.BaseResponse +import com.paw.key.data.dto.response.LoginResponseDto + +interface AuthRemoteDataSource { + suspend fun login(providerToken: String, provider: String): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt index 8e7edb55..7cd8c8d2 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/login/GoogleAuthDataSource.kt @@ -8,7 +8,3 @@ import com.paw.key.data.dto.response.LoginResponseDto interface GoogleAuthDataSource { suspend fun signIn(context: Context): Result } - -interface AuthRemoteDataSource { - suspend fun login(providerToken: String, provider: String): BaseResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt index b738422f..451e61a6 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt @@ -13,8 +13,7 @@ class RegionCurrentRepositoryImpl @Inject constructor( return runCatching { val response = dataSource.regionCurrent(userId) if (response.code == "S000") { - // 올바른 타입 반환 (RegionCurrentDataEntity) - response.data.toEntity() // DTO에서 Entity로 변환 + response.data.toEntity() } else { throw Exception(response.message) } diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt index ccb7bb72..b0b6a1bb 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.paw.key.data.repositoryimpl.login import android.content.Context -import com.paw.key.core.util.PreferenceDataStore +import com.paw.key.core.util.UserDataStore import com.paw.key.core.util.suspendRunCatching import com.paw.key.data.dto.response.LoginResponseDto import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource @@ -12,17 +12,24 @@ import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( private val authRemoteDataSource: AuthRemoteDataSource, private val googleAuthDataSource: GoogleAuthDataSource, + private val context: Context ) : AuthRepository { + override suspend fun signInWithGoogle(context: Context): Result = googleAuthDataSource.signIn(context).map { it.idToken } override suspend fun login(providerToken: String, provider: String): Result = suspendRunCatching { val loginResponse = authRemoteDataSource.login(providerToken, provider).data - PreferenceDataStore.saveTokens( - accessToken = loginResponse.AccessToken, - refreshToken = loginResponse.RefreshToken + + UserDataStore.saveAcessToken( + context = context, + token = loginResponse.AccessToken + ) + UserDataStore.saveRefreshToken( + context = context, + token = loginResponse.RefreshToken ) loginResponse } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt b/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt index c5781e95..fe31c3c4 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt @@ -2,23 +2,21 @@ package com.paw.key.presentation.ui.login.state import androidx.compose.runtime.Immutable -class LoginContract { - @Immutable - data class LoginState( - val email: String = "", - val password: String = "", +@Immutable +data class LoginState( + val email: String = "", + val password: String = "", - val isPasswordVisible: Boolean = false - ) { - val isLoginValid get() = email.isNotBlank() && password.isNotBlank() - } + val isPasswordVisible: Boolean = false, +) { + val isLoginValid get() = email.isNotBlank() && password.isNotBlank() +} - sealed class LoginSideEffect { - data class ShowSnackBar(val message: String) : LoginSideEffect() - data object NavigateUp: LoginSideEffect() - data object NavigateNext: LoginSideEffect() +sealed class LoginSideEffect { + data class ShowSnackBar(val message: String) : LoginSideEffect() + data object NavigateUp : LoginSideEffect() + data object NavigateNext : LoginSideEffect() - data object SignInSucceed : LoginSideEffect() - data object SignInFailed : LoginSideEffect() - } + data object SignInSucceed : LoginSideEffect() + data object SignInFailed : LoginSideEffect() } \ No newline at end of file From 139c1220faff491c7d3f72185b69e734026f88e2 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Mon, 22 Sep 2025 19:00:16 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feature/#135:=20security=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e17055ba..eacc6bf6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -116,4 +116,7 @@ dependencies { implementation(libs.googleid) implementation(libs.androidx.credentials.play.services.auth) coreLibraryDesugaring(libs.desugar.jdk.libs) + + // 암호화 + implementation(libs.androidx.security) } \ No newline at end of file From 17259d56a37f396021c3e57f9a15367bfda53a0c Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Mon, 22 Sep 2025 19:00:47 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feature/#135:=20datastore=20=EC=95=94?= =?UTF-8?q?=ED=98=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paw/key/core/util/PreferenceDataStore.kt | 105 +++++++++--------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt index ffe6d73c..3a16757a 100644 --- a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt +++ b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt @@ -1,12 +1,15 @@ package com.paw.key.core.util import android.content.Context +import android.content.SharedPreferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey import com.naver.maps.geometry.LatLng import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -32,10 +35,6 @@ 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") - private fun List.toPreferenceString(): String = joinToString(";") { "${it.latitude},${it.longitude}" } @@ -120,7 +119,7 @@ object PreferenceDataStore { userId: Int, userName: String, petId: Int, - petName: String + petName: String, ) { summaryStore.edit { it[USER_ID_KEY] = userId @@ -150,7 +149,7 @@ object PreferenceDataStore { val userId: Int, val userName: String, val petId: Int, - val petName: String + val petName: String, ) fun getUserInfo(): Flow = summaryStore.data.map { @@ -180,7 +179,7 @@ object PreferenceDataStore { guId: Int, dongId: Int, guName: String, - dongName: String + dongName: String, ) { summaryStore.edit { preferences -> preferences[SELECTED_GU_ID_KEY] = guId @@ -249,7 +248,7 @@ object PreferenceDataStore { val dongId: Int, val guName: String, val dongName: String, - val activeRegion: String + val activeRegion: String, ) { val displayLocation: String get() = if (guName.isNotEmpty() && dongName.isNotEmpty()) { @@ -289,65 +288,61 @@ object PreferenceDataStore { preferences.remove(ACTIVE_REGION_KEY) } } +} - // ===== 토큰 관리 관련 함수들 ===== +object UserDataStore { + private val ACCESS_TOKEN = "ACCESS_TOKEN" + private val REFRESH_TOKEN = "REFRESH_TOKEN" + private val PREFERENCES_NAME = "user_preferences" - /** - * 액세스 토큰과 리프레시 토큰을 저장합니다 - */ - suspend fun saveTokens(accessToken: String, refreshToken: String) { - summaryStore.edit { preferences -> - preferences[ACCESS_TOKEN_KEY] = accessToken - preferences[REFRESH_TOKEN_KEY] = refreshToken - } + private fun getSharedPreferences(context: Context): SharedPreferences { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + return EncryptedSharedPreferences.create( + context, + PREFERENCES_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) } - /** - * 액세스 토큰을 조회합니다 - */ - fun getAccessToken(): Flow = summaryStore.data.map { - it[ACCESS_TOKEN_KEY] ?: "" + fun saveAcessToken(context: Context, token: String) { + val sharedPreferences = getSharedPreferences(context) + with(sharedPreferences.edit()) { + putString(ACCESS_TOKEN, token) + commit() + } } - /** - * 리프레시 토큰을 조회합니다 - */ - fun getRefreshToken(): Flow = summaryStore.data.map { - it[REFRESH_TOKEN_KEY] ?: "" + fun saveRefreshToken(context: Context, token: String) { + val sharedPreferences = getSharedPreferences(context) + with(sharedPreferences.edit()) { + putString(REFRESH_TOKEN, token) + commit() + } } - /** - * 토큰 정보 데이터 클래스 - */ - data class TokenInfo( - val accessToken: String, - val refreshToken: String, - ) { - val isTokensAvailable: Boolean - get() = accessToken.isNotEmpty() && refreshToken.isNotEmpty() + fun getAccessToken(context: Context): String { + val sharedPreferences = getSharedPreferences(context) + return sharedPreferences.getString(ACCESS_TOKEN, "") ?: "" } - /** - * 모든 토큰 정보를 한번에 조회합니다 - */ - fun getTokenInfo(): Flow = summaryStore.data.map { preferences -> - TokenInfo( - accessToken = preferences[ACCESS_TOKEN_KEY] ?: "", - refreshToken = preferences[REFRESH_TOKEN_KEY] ?: "" - ) + fun getRefreshToken(context: Context): String { + val sharedPreferences = getSharedPreferences(context) + return sharedPreferences.getString(REFRESH_TOKEN, "") ?: "" } - /** - * 토큰 정보를 초기화합니다 - */ - suspend fun clearTokens() { - summaryStore.edit { preferences -> - preferences.remove(ACCESS_TOKEN_KEY) - preferences.remove(REFRESH_TOKEN_KEY) + fun removeToken(context: Context) { + val sharedPreferences = getSharedPreferences(context) + with(sharedPreferences.edit()) { + remove(ACCESS_TOKEN) + remove(REFRESH_TOKEN) + commit() } } +} + - suspend fun clearAllData() { - summaryStore.edit { it.clear() } - } -} \ No newline at end of file From fe270d47fa3c7c80f45d659258b06853c946eaf9 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Mon, 22 Sep 2025 19:07:05 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feature/#135:=20datastore=20=EC=95=94?= =?UTF-8?q?=ED=98=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/login/viewmodel/LoginViewModel.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt index a05c8a49..4de58106 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt @@ -1,9 +1,9 @@ package com.paw.key.presentation.ui.login.viewmodel import androidx.lifecycle.ViewModel -import androidx.navigation.NavController import com.paw.key.domain.repository.login.AuthRepository -import com.paw.key.presentation.ui.login.state.LoginContract +import com.paw.key.presentation.ui.login.state.LoginSideEffect +import com.paw.key.presentation.ui.login.state.LoginState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -12,12 +12,12 @@ import javax.inject.Inject class LoginViewModel @Inject constructor( private val authRepository: AuthRepository, ) : ViewModel() { - private val _state = MutableStateFlow(LoginContract.LoginState()) - val state: StateFlow + private val _state = MutableStateFlow(LoginState()) + val state: StateFlow get() = _state.asStateFlow() - private val _sideEffect = MutableStateFlow(null) - val sideEffect: StateFlow + private val _sideEffect = MutableStateFlow(null) + val sideEffect: StateFlow get() = _sideEffect.asStateFlow() fun onEmailChanged(email: String) { @@ -38,4 +38,5 @@ class LoginViewModel @Inject constructor( ) } + } \ No newline at end of file