From 2910fc04500856e80f270d208510029dedd6d6ac Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 16:52:46 +0900 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20datastore=EC=97=90=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=95=84=EC=9D=B4=EB=94=94=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/local/UserInfoStorage.kt | 21 +++++++++++++++++++ .../app/ui/diary/view/DiaryFragment.kt | 2 -- app/src/main/res/layout/fragment_account.xml | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt index 45627162..7852512b 100644 --- a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt +++ b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt @@ -48,6 +48,25 @@ class UserInfoStorage @Inject constructor( } } + /** + * USER ID 저장 + */ + suspend fun saveUserId(id: String) { + dataStore.edit { preferences -> + preferences[USER_ID] = id + } + } + + /** + * USER ID 읽기 + */ + + fun getUserId(): Flow { + return dataStore.data.map { preferences -> + preferences[USER_ID] + } + } + /** * Access Token 저장 */ @@ -153,6 +172,8 @@ class UserInfoStorage @Inject constructor( companion object { private val LOGIN_TYPE = stringPreferencesKey("login_type") + + private val USER_ID = stringPreferencesKey("user_id") private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") private val KEY_REFRESH_TOKEN = stringPreferencesKey("refresh_token") private val KEY_RECOVER_TOKEN = stringPreferencesKey("recover_token") diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt index 5b03458c..4dd4ea70 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryFragment.kt @@ -214,8 +214,6 @@ snackBar.show() } - - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 3f70055c..8d05c8c1 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -81,7 +81,7 @@ - + Date: Tue, 10 Feb 2026 17:51:55 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EB=94=94=20=EC=BA=90=EC=8B=B1=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/api/AccountApiService.kt | 12 ++++++ .../egobook/app/data/local/UserInfoStorage.kt | 7 +--- .../app/data/model/account/AccountResponse.kt | 10 +++++ .../account/AccountRepositoryImpl.kt | 37 +++++++++++++++++++ .../java/com/egobook/app/di/ServiceModule.kt | 7 ++++ .../java/com/egobook/app/domain/model/User.kt | 5 --- .../repository/account/AccountRepository.kt | 9 +++++ 7 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/data/api/AccountApiService.kt create mode 100644 app/src/main/java/com/egobook/app/data/model/account/AccountResponse.kt create mode 100644 app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt delete mode 100644 app/src/main/java/com/egobook/app/domain/model/User.kt create mode 100644 app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt diff --git a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt new file mode 100644 index 00000000..7a51d20a --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt @@ -0,0 +1,12 @@ +package com.egobook.app.data.api + +import com.egobook.app.data.model.ApiResponse +import com.egobook.app.data.model.account.AccountResponse +import retrofit2.http.GET +import retrofit2.Response + +interface AccountApiService { + //유저 id 불러오기 + @GET("/home/settings") + suspend fun getUserId(): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt index 7852512b..ea709e6b 100644 --- a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt +++ b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt @@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.egobook.app.domain.model.User import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -145,7 +144,6 @@ class UserInfoStorage @Inject constructor( suspend fun saveAllTokens( accessToken: String, refreshToken: String, - idToken: String? = null, recoverToken: String? = null, ) { dataStore.edit { preferences -> @@ -156,7 +154,7 @@ class UserInfoStorage @Inject constructor( } /** - * 모든 데이터 삭제 (로그아웃 시 사용) + * 모든 데이터 삭제 (로그아웃 및 회원탈퇴 시 사용) */ suspend fun clearAll() { dataStore.edit { preferences -> @@ -164,6 +162,7 @@ class UserInfoStorage @Inject constructor( } } + // 로그인 타입 정의 enum class LoginType { GOOGLE, GUEST } @@ -172,13 +171,11 @@ class UserInfoStorage @Inject constructor( companion object { private val LOGIN_TYPE = stringPreferencesKey("login_type") - private val USER_ID = stringPreferencesKey("user_id") private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") private val KEY_REFRESH_TOKEN = stringPreferencesKey("refresh_token") private val KEY_RECOVER_TOKEN = stringPreferencesKey("recover_token") private val KEY_DEVICE_UID = stringPreferencesKey("device_uid") - } } diff --git a/app/src/main/java/com/egobook/app/data/model/account/AccountResponse.kt b/app/src/main/java/com/egobook/app/data/model/account/AccountResponse.kt new file mode 100644 index 00000000..a92ffc78 --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/account/AccountResponse.kt @@ -0,0 +1,10 @@ +package com.egobook.app.data.model.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AccountResponse( + @SerialName("accountCode") + val accountCode: String, +) diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt new file mode 100644 index 00000000..518094ae --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.egobook.app.data.repository.account + +import com.egobook.app.data.api.AccountApiService +import com.egobook.app.data.local.UserInfoStorage +import com.egobook.app.data.util.safeApiCall +import com.egobook.app.domain.repository.account.AccountRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.firstOrNull + +class AccountRepositoryImpl @Inject constructor( + private val apiService: AccountApiService, + private val userInfoStorage: UserInfoStorage +) : AccountRepository { + + override suspend fun getUserId(): Result { + + //datastore에 데이터가 있는지 먼저 체크 + val localUserId = userInfoStorage.getUserId().firstOrNull() + if (!localUserId.isNullOrBlank()) { + return Result.success(localUserId) + } + + //datastore에 데이터가 없다면 api호출 + val result = safeApiCall( + apiCall = { apiService.getUserId() }, + transform = { it.accountCode } + ) + + //응답 값을 userId 저장 + result.onSuccess { userId -> + userInfoStorage.saveUserId(userId) + } + + return result + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/di/ServiceModule.kt b/app/src/main/java/com/egobook/app/di/ServiceModule.kt index 0e826898..b33ec34a 100644 --- a/app/src/main/java/com/egobook/app/di/ServiceModule.kt +++ b/app/src/main/java/com/egobook/app/di/ServiceModule.kt @@ -1,5 +1,6 @@ package com.egobook.app.di +import com.egobook.app.data.api.AccountApiService import com.egobook.app.data.api.AuthApiService import com.egobook.app.data.api.CounselingApiService import com.egobook.app.data.api.DiaryApiService @@ -45,6 +46,12 @@ object ServiceModule { fun provideAuthService(retrofit: Retrofit): AuthApiService = retrofit.create(AuthApiService::class.java) + @Provides + @Singleton + fun provideAccountService(retrofit: Retrofit): AccountApiService = + retrofit.create(AccountApiService::class.java) + + @Provides @Singleton fun provideDiaryService(retrofit: Retrofit): DiaryApiService = diff --git a/app/src/main/java/com/egobook/app/domain/model/User.kt b/app/src/main/java/com/egobook/app/domain/model/User.kt deleted file mode 100644 index f57ad699..00000000 --- a/app/src/main/java/com/egobook/app/domain/model/User.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.egobook.app.domain.model - -data class User( - val id: String -) diff --git a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt new file mode 100644 index 00000000..2709a56e --- /dev/null +++ b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt @@ -0,0 +1,9 @@ +package com.egobook.app.domain.repository.account + +interface AccountRepository { + + /** + * 유저 id 조회 + */ + suspend fun getUserId(): Result +} \ No newline at end of file From 8864921cc8ad8727d81bc2288b45788d4e3cbd80 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 18:31:16 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20ID=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/AccountRepositoryImpl.kt | 4 +- .../com/egobook/app/di/RepositoryModule.kt | 7 +- .../java/com/egobook/app/ui/account/.gitkeep | 0 .../{ => view}/AccountBottomSheetFragment.kt | 5 +- .../ui/account/{ => view}/AccountFragment.kt | 64 +++++++++++++++---- .../ui/account/viewmodel/AccountViewModel.kt | 41 ++++++++++++ app/src/main/res/layout/fragment_account.xml | 2 +- .../main/res/navigation/bottom_navigation.xml | 2 +- 8 files changed, 105 insertions(+), 20 deletions(-) delete mode 100644 app/src/main/java/com/egobook/app/ui/account/.gitkeep rename app/src/main/java/com/egobook/app/ui/account/{ => view}/AccountBottomSheetFragment.kt (92%) rename app/src/main/java/com/egobook/app/ui/account/{ => view}/AccountFragment.kt (54%) create mode 100644 app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index 518094ae..bd3bc4b8 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -16,6 +16,8 @@ class AccountRepositoryImpl @Inject constructor( //datastore에 데이터가 있는지 먼저 체크 val localUserId = userInfoStorage.getUserId().firstOrNull() + + //있다면 바로 리턴 if (!localUserId.isNullOrBlank()) { return Result.success(localUserId) } @@ -26,7 +28,7 @@ class AccountRepositoryImpl @Inject constructor( transform = { it.accountCode } ) - //응답 값을 userId 저장 + //응답 값을 캐싱 result.onSuccess { userId -> userInfoStorage.saveUserId(userId) } diff --git a/app/src/main/java/com/egobook/app/di/RepositoryModule.kt b/app/src/main/java/com/egobook/app/di/RepositoryModule.kt index b0470a7a..0bba0d3f 100644 --- a/app/src/main/java/com/egobook/app/di/RepositoryModule.kt +++ b/app/src/main/java/com/egobook/app/di/RepositoryModule.kt @@ -6,6 +6,7 @@ import com.egobook.app.data.repository.NotificationRepositoryImpl import com.egobook.app.domain.repository.CounselingRepository import com.egobook.app.data.repository.auth.AuthRepositoryImpl import com.egobook.app.data.repository.QuestionRepositoryImpl +import com.egobook.app.data.repository.account.AccountRepositoryImpl import com.egobook.app.data.repository.diary.DiaryRepositoryImpl import com.egobook.app.domain.repository.FriendsRepository import com.egobook.app.domain.repository.NotificationRepository @@ -14,6 +15,7 @@ import com.egobook.app.ui.shop.NetworkStoreRepository import com.egobook.app.ui.shop.StoreRepository import dagger.Binds import com.egobook.app.domain.repository.QuestionRepository +import com.egobook.app.domain.repository.account.AccountRepository import com.egobook.app.domain.repository.diary.DiaryRepository import com.egobook.app.ui.home.repository.NetworkTendencyLevelService import com.egobook.app.ui.home.repository.NetworkUserRepository @@ -50,8 +52,11 @@ abstract class RepositoryModule { @Binds @Singleton - abstract fun bindDiaryRepository(impl: DiaryRepositoryImpl): DiaryRepository + abstract fun bindAccountRepository(impl: AccountRepositoryImpl): AccountRepository + @Binds + @Singleton + abstract fun bindDiaryRepository(impl: DiaryRepositoryImpl): DiaryRepository @Binds @Singleton diff --git a/app/src/main/java/com/egobook/app/ui/account/.gitkeep b/app/src/main/java/com/egobook/app/ui/account/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/egobook/app/ui/account/AccountBottomSheetFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt similarity index 92% rename from app/src/main/java/com/egobook/app/ui/account/AccountBottomSheetFragment.kt rename to app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt index 911179d9..4488f951 100644 --- a/app/src/main/java/com/egobook/app/ui/account/AccountBottomSheetFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt @@ -1,4 +1,4 @@ -package com.egobook.app.ui.account +package com.egobook.app.ui.account.view import android.content.DialogInterface import android.os.Bundle @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.egobook.app.databinding.FragmentAccountBottomSheetBinding +import com.egobook.app.ui.account.view.AccountFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment class AccountBottomSheetFragment : BottomSheetDialogFragment() { @@ -38,4 +39,4 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { companion object { const val TAG = "AccountBottomSheet" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/account/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt similarity index 54% rename from app/src/main/java/com/egobook/app/ui/account/AccountFragment.kt rename to app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index e2faf752..7f4d3ee4 100644 --- a/app/src/main/java/com/egobook/app/ui/account/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -1,23 +1,30 @@ -package com.egobook.app.ui.account +package com.egobook.app.ui.account.view -import android.graphics.Color -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.WindowInsetsController -import androidx.core.content.ContextCompat +import android.widget.Toast import androidx.fragment.app.Fragment -import com.egobook.app.R +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import com.egobook.app.R import com.egobook.app.databinding.FragmentAccountBinding +import com.egobook.app.ui.account.viewmodel.AccountViewModel +import com.egobook.app.util.UiState +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +@AndroidEntryPoint class AccountFragment : Fragment() { private var _binding: FragmentAccountBinding? = null private val binding get() = _binding!! + private val viewModel: AccountViewModel by viewModels() private val blurRadius = 5f override fun onCreateView( @@ -32,15 +39,31 @@ class AccountFragment : Fragment() { super.onViewCreated(view, savedInstanceState) setClickListeners() setupBlur() + observeUserIdState() } - private fun setupBlur() { - binding.blurView.setupWith(binding.blurTarget) - .setBlurRadius(blurRadius) - .setBlurAutoUpdate(true) - } - fun clearBlur() { - binding.blurView.visibility = View.GONE + private fun observeUserIdState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.userIdState.collect { state -> + when (state) { + + is UiState.Idle -> Unit + + is UiState.Loading -> { + } + + is UiState.Success -> { + binding.tvRealAccountId.text = state.data + } + + is UiState.Failure -> { + Toast.makeText(requireContext(), "유저 id를 가져올 수 없습니다.", Toast.LENGTH_SHORT).show() + } + } + } + } + } } @@ -63,4 +86,17 @@ class AccountFragment : Fragment() { super.onDestroyView() _binding = null } -} + + + //=============다이알로그 출력용 블러뷰 세팅==================== + + private fun setupBlur() { + binding.blurView.setupWith(binding.blurTarget) + .setBlurRadius(blurRadius) + .setBlurAutoUpdate(true) + } + fun clearBlur() { + binding.blurView.visibility = View.GONE + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt new file mode 100644 index 00000000..2f6cdb3a --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -0,0 +1,41 @@ +package com.egobook.app.ui.account.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.egobook.app.domain.repository.account.AccountRepository +import com.egobook.app.util.UiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AccountViewModel @Inject constructor( + private val accountRepository: AccountRepository +) : ViewModel() { + private val _userIdState = MutableStateFlow>(UiState.Idle) + val userIdState = _userIdState.asStateFlow() + + init { + getUserId() + } + + fun getUserId() { + viewModelScope.launch { + _userIdState.value = UiState.Loading + + accountRepository.getUserId() + .onSuccess { id -> + _userIdState.value = UiState.Success(id) + } + .onFailure { e -> + _userIdState.value = + UiState.Failure(e.message ?: "사용자 ID를 불러오지 못했습니다") + } + } + } + +} diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 8d05c8c1..1f34ff2a 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.account.AccountFragment"> + tools:context=".ui.account.view.AccountFragment"> + android:name="com.egobook.app.ui.account.view.AccountFragment"> Date: Tue, 10 Feb 2026 18:47:13 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=ED=99=95=EC=9D=B8=20=EC=BD=9C=EB=B0=B1=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/account/view/AccountBottomSheetFragment.kt | 14 ++++++++++++++ .../egobook/app/ui/account/view/AccountFragment.kt | 1 - 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt index 4488f951..1d10769e 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt @@ -14,6 +14,20 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { private var _binding: FragmentAccountBottomSheetBinding? = null private val binding get() = _binding!! + //연동 확인 콜백 인터페이스 + interface OnLinkConfirmListener { + fun onLinkConfirmed() + } + + private var linkConfirmListener: OnLinkConfirmListener? = null + + fun setOnLinkConfirmListener(listener: OnLinkConfirmListener) { + linkConfirmListener = listener + } + + + + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 7f4d3ee4..ec8e1ade 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -66,7 +66,6 @@ class AccountFragment : Fragment() { } } - private fun setClickListeners() { binding.apply { btnBack.setOnClickListener { From b45850167cc4014091556ede66c738dfe4ce8c6a Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 19:05:12 +0900 Subject: [PATCH 05/22] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8&?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=91=9C=EC=A4=80=20ApiResponse=20=ED=8C=A8=ED=84=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/api/AuthApiService.kt | 21 +- .../data/interceptor/TokenAuthenticator.kt | 16 +- .../app/data/model/auth/AuthResponse.kt | 92 +------- .../repository/auth/AuthRepositoryImpl.kt | 214 ++++++++---------- .../egobook/app/data/util/ApiResponseExt.kt | 20 ++ 5 files changed, 141 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/api/AuthApiService.kt b/app/src/main/java/com/egobook/app/data/api/AuthApiService.kt index 538f9c0e..0f42a010 100644 --- a/app/src/main/java/com/egobook/app/data/api/AuthApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/AuthApiService.kt @@ -1,16 +1,13 @@ package com.egobook.app.data.api +import com.egobook.app.data.model.ApiResponse import com.egobook.app.data.model.auth.AccessTokenRequest -import com.egobook.app.data.model.auth.AccessTokenResponse -import com.egobook.app.data.model.auth.TokensRequestAgainByGuest +import com.egobook.app.data.model.auth.GuestTokenData +import com.egobook.app.data.model.auth.TokenData import com.egobook.app.data.model.auth.TokenRequestByGoogle import com.egobook.app.data.model.auth.TokenRequestByGuest -import com.egobook.app.data.model.auth.TokenResponseAgainByGuest -import com.egobook.app.data.model.auth.TokenResponseByGoogle -import com.egobook.app.data.model.auth.TokenResponseByGuest import com.egobook.app.data.model.auth.TokensRequest -import com.egobook.app.data.model.auth.TokensResponse -import retrofit2.Response +import com.egobook.app.data.model.auth.TokensRequestAgainByGuest import retrofit2.http.Body import retrofit2.http.POST @@ -20,30 +17,30 @@ interface AuthApiService { @POST("auth/google/join") suspend fun googleSignUp( @Body request: TokenRequestByGoogle - ): Response + ): ApiResponse //Guest 최초 둘러보기 @POST("auth/guest/join") suspend fun guestLogin( @Body request: TokenRequestByGuest - ): Response + ): ApiResponse //액세스토큰 재발급 @POST("auth/refresh") suspend fun getAccessToken( @Body request: AccessTokenRequest - ): Response + ): ApiResponse //Tokens 재발급 - refreshToken까지 만료시 @POST("auth/google/recertification") suspend fun reGetTokens( @Body request: TokensRequest - ): Response + ): ApiResponse //Guest로그인 상태에서 Tokens 재발급 @POST("auth/google/recertification") suspend fun reGetTokensByGuest( @Body request: TokensRequestAgainByGuest - ): Response + ): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt b/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt index 8dadce17..ea16dc1b 100644 --- a/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt +++ b/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt @@ -60,13 +60,17 @@ class TokenAuthenticator @Inject constructor( ) ) - Timber.d("토큰 갱신 API 응답: 코드=${tokenResponse.code()}, 성공=${tokenResponse.isSuccessful}") + Timber.d("토큰 갱신 API 응답: 코드=${tokenResponse.code}, 메시지=${tokenResponse.message}") - if (tokenResponse.isSuccessful && tokenResponse.body() != null) { - val newAccessToken = tokenResponse.body()!!.data.accessToken + if (tokenResponse.code == "SUCCESS") { + val newAccessToken = tokenResponse.data.accessToken + val newRefreshToken = tokenResponse.data.refreshToken - // 새 액세스 토큰을 DataStore에 저장 - userInfoStorage.saveAccessToken(newAccessToken) + // 새 토큰들을 DataStore에 저장 + userInfoStorage.saveAllTokens( + accessToken = newAccessToken, + refreshToken = newRefreshToken + ) Timber.d("액세스 토큰 갱신 성공") @@ -78,7 +82,7 @@ class TokenAuthenticator @Inject constructor( } else { // 리프레시 토큰 갱신 실패 -> 리프레시 토큰 만료로 판단 - Timber.e("리프레시 토큰 갱신 실패: ${tokenResponse.code()}, 로그아웃 처리") + Timber.e("리프레시 토큰 갱신 실패: ${tokenResponse.code}, 로그아웃 처리") handleLogout() null } diff --git a/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt b/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt index a1a40bdf..9d1cc3d6 100644 --- a/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt @@ -3,90 +3,10 @@ package com.egobook.app.data.model.auth import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -//Google 최초 회원가입 -@Serializable -data class TokenResponseByGoogle( - @SerialName("status") - val status: Int, - - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: TokenData -) - -//Guest 최초 둘러보기 -@Serializable -data class TokenResponseByGuest( - @SerialName("status") - val status: Int, - - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: GuestTokenData -) - -//액세스토큰 재발급 -@Serializable -data class AccessTokenResponse( - @SerialName("status") - val status: Int, - - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: TokenData -) - -//Tokens 재발급 - refreshToken까지 만료시 -@Serializable -data class TokensResponse( - @SerialName("status") - val status: Int, - - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: TokenData -) - -//Guest로그인 상태에서 Token 재발급 -@Serializable -data class TokenResponseAgainByGuest( - @SerialName("status") - val status: Int, - - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: GuestTokenData - -) - - -//=============================================================== - +/** + * 표준 Google 로그인/회원가입 토큰 응답 데이터 + * ApiResponse 형태로 사용됨 + */ @Serializable data class TokenData( @SerialName("accessToken") @@ -96,6 +16,10 @@ data class TokenData( val refreshToken: String ) +/** + * Guest 로그인 토큰 응답 데이터 (recoverToken 포함) + * ApiResponse 형태로 사용됨 + */ @Serializable data class GuestTokenData( @SerialName("accessToken") diff --git a/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt index 40deaaeb..7aa72399 100644 --- a/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt @@ -7,6 +7,7 @@ import com.egobook.app.data.model.auth.TokensRequest import com.egobook.app.data.model.auth.TokenRequestByGoogle import com.egobook.app.data.model.auth.TokenRequestByGuest import com.egobook.app.data.model.auth.TokensRequestAgainByGuest +import com.egobook.app.data.util.safeApiCallWithSuspendTransform import com.egobook.app.domain.repository.auth.AuthRepository import timber.log.Timber import javax.inject.Inject @@ -19,14 +20,13 @@ class AuthRepositoryImpl @Inject constructor( ) : AuthRepository { override suspend fun googleSignUp(idToken: String): Result { - return try { - val response = apiService.googleSignUp( - TokenRequestByGoogle(idToken = idToken) - ) - - if (response.isSuccessful && response.body() != null) { - val tokenData = response.body()!!.data - + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.googleSignUp( + TokenRequestByGoogle(idToken = idToken) + ) + }, + transform = { tokenData -> userInfoStorage.saveAllTokens( accessToken = tokenData.accessToken, refreshToken = tokenData.refreshToken @@ -34,31 +34,24 @@ class AuthRepositoryImpl @Inject constructor( val loginType = UserInfoStorage.LoginType.GOOGLE userInfoStorage.saveLoginType(loginType) Timber.d("구글 회원가입 성공, loginType=$loginType") - Result.success(Unit) - } else { - Result.failure(Exception("회원가입 요청 실패: ${response.code()}")) + Unit } - - } catch (e: Exception) { - Result.failure(e) - } + ) } override suspend fun guestLogin(): Result { - return try { - // 앱 설치 인스턴스 고유 UUID - val deviceUid = UUID.randomUUID().toString() - - Timber.d("게스트 로그인 시도: deviceUid=$deviceUid") - - // Guest 로그인 API 요청 - val response = apiService.guestLogin( - TokenRequestByGuest(deviceUid = deviceUid) - ) - - if (response.isSuccessful && response.body() != null) { - val tokenData = response.body()!!.data - + // 앱 설치 인스턴스 고유 UUID + val deviceUid = UUID.randomUUID().toString() + + Timber.d("게스트 로그인 시도: deviceUid=$deviceUid") + + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.guestLogin( + TokenRequestByGuest(deviceUid = deviceUid) + ) + }, + transform = { tokenData -> // UUID 저장 userInfoStorage.saveDeviceUid(deviceUid) @@ -73,127 +66,108 @@ class AuthRepositoryImpl @Inject constructor( val loginType = UserInfoStorage.LoginType.GUEST userInfoStorage.saveLoginType(loginType) Timber.d("게스트 로그인 성공, loginType=$loginType") - Result.success(Unit) - } else { - Timber.e("게스트 로그인 실패: ${response.code()}") - Result.failure(Exception("게스트 로그인 실패: ${response.code()}")) + Unit + } + ).also { result -> + if (result.isFailure) { + Timber.e(result.exceptionOrNull(), "게스트 로그인 중 오류") } - } catch (e: Exception) { - Timber.e(e, "게스트 로그인 중 오류") - Result.failure(e) } } override suspend fun refreshAccessToken(): Result { - return try { - // Access, Refresh Token 읽기 - val accessToken = userInfoStorage.getAccessToken().first() - ?: return Result.failure(Exception("액세스 토큰을 찾을 수 없습니다.")) - - val refreshToken = userInfoStorage.getRefreshToken().first() - ?: return Result.failure(Exception("리프레시 토큰을 찾을 수 없습니다.")) - - Timber.d("액세스 토큰 재발급 시도 시작") - - // API 요청 - val response = apiService.getAccessToken( - AccessTokenRequest( - accessToken = accessToken, - refreshToken = refreshToken + // Access, Refresh Token 읽기 + val accessToken = userInfoStorage.getAccessToken().first() + ?: return Result.failure(Exception("액세스 토큰을 찾을 수 없습니다.")) + + val refreshToken = userInfoStorage.getRefreshToken().first() + ?: return Result.failure(Exception("리프레시 토큰을 찾을 수 없습니다.")) + + Timber.d("액세스 토큰 재발급 시도 시작") + + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.getAccessToken( + AccessTokenRequest( + accessToken = accessToken, + refreshToken = refreshToken + ) ) - ) - - Timber.d("응답 코드: ${response.code()}") - Timber.d("응답 성공 여부: ${response.isSuccessful}") - - // 응답 성공시 - if (response.isSuccessful && response.body() != null) { - val tokenData = response.body()!!.data + }, + transform = { tokenData -> userInfoStorage.saveAllTokens( accessToken = tokenData.accessToken, refreshToken = tokenData.refreshToken ) - Result.success(Unit) - } else { - Result.failure(Exception("액세스 토큰 재발급 실패: ${response.code()}")) + Timber.d("액세스 토큰 재발급 성공") + Unit + } + ).also { result -> + if (result.isFailure) { + Timber.e(result.exceptionOrNull(), "액세스 토큰 재발급 중 오류") } - } catch (e: Exception) { - Timber.e(e, "액세스 토큰 재발급 중 오류") - Result.failure(e) } } override suspend fun refreshTokens(idToken: String): Result { - return try { - // 액세스 토큰 가져오기 (없으면 null) - val accessToken = userInfoStorage.getAccessToken().first() - - val response = apiService.reGetTokens( - TokensRequest( - idToken = idToken, - accessToken = accessToken + // 액세스 토큰 가져오기 (없으면 null) + val accessToken = userInfoStorage.getAccessToken().first() + + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.reGetTokens( + TokensRequest( + idToken = idToken, + accessToken = accessToken + ) ) - ) - - if (response.isSuccessful && response.body() != null) { - val tokenData = response.body()!!.data - + }, + transform = { tokenData -> userInfoStorage.saveAllTokens( - accessToken = tokenData.accessToken, + accessToken = tokenData.accessToken, refreshToken = tokenData.refreshToken ) - - Result.success(Unit) - } else { - Result.failure(Exception("토큰 갱신 요청 실패: ${response.code()}")) + Unit } - - } catch (e: Exception) { - Result.failure(e) - } + ) } override suspend fun refreshGuestTokens(): Result { - return try { - // Device UID 및 Access Token, Recover Token 읽기 - val deviceUid = userInfoStorage.getDeviceUid().first() - ?: return Result.failure(Exception("디바이스 UID를 찾을 수 없습니다.")) - - val accessToken = userInfoStorage.getAccessToken().first() - ?: return Result.failure(Exception("accessToken을 찾을 수 없습니다.")) - - val recoverToken = userInfoStorage.getRecoverToken().first() - ?: return Result.failure(Exception("recoverToken을 찾을 수 없습니다.")) - - Timber.d("게스트 토큰 재발급 시도") - - // API 요청 - val response = apiService.reGetTokensByGuest( - TokensRequestAgainByGuest( - deviceUid = deviceUid, - accessToken = accessToken, - recoverToken = recoverToken + // Device UID 및 Access Token, Recover Token 읽기 + val deviceUid = userInfoStorage.getDeviceUid().first() + ?: return Result.failure(Exception("디바이스 UID를 찾을 수 없습니다.")) + + val accessToken = userInfoStorage.getAccessToken().first() + ?: return Result.failure(Exception("accessToken을 찾을 수 없습니다.")) + + val recoverToken = userInfoStorage.getRecoverToken().first() + ?: return Result.failure(Exception("recoverToken을 찾을 수 없습니다.")) + + Timber.d("게스트 토큰 재발급 시도") + + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.reGetTokensByGuest( + TokensRequestAgainByGuest( + deviceUid = deviceUid, + accessToken = accessToken, + recoverToken = recoverToken + ) ) - ) - - Timber.d("응답 코드: ${response.code()}") - Timber.d("응답 성공 여부: ${response.isSuccessful}") - - // 응답 성공시 - if (response.isSuccessful && response.body() != null) { - val tokenData = response.body()!!.data + }, + transform = { tokenData -> userInfoStorage.saveAllTokens( accessToken = tokenData.accessToken, refreshToken = tokenData.refreshToken, recoverToken = tokenData.recoverToken ) - Result.success(Unit) - } else { - Result.failure(Exception("게스트 토큰 재발급 실패: ${response.code()}")) + Timber.d("게스트 토큰 재발급 성공") + Unit + } + ).also { result -> + if (result.isFailure) { + Timber.e(result.exceptionOrNull(), "게스트 토큰 재발급 중 오류") } - } catch (e: Exception) { - Timber.e(e, "게스트 토큰 재발급 중 오류") - Result.failure(e) } } diff --git a/app/src/main/java/com/egobook/app/data/util/ApiResponseExt.kt b/app/src/main/java/com/egobook/app/data/util/ApiResponseExt.kt index 28d47cef..7d215d6a 100644 --- a/app/src/main/java/com/egobook/app/data/util/ApiResponseExt.kt +++ b/app/src/main/java/com/egobook/app/data/util/ApiResponseExt.kt @@ -56,6 +56,26 @@ suspend inline fun safeApiCall( } } +/** + * API 호출을 안전하게 실행하고 변환하는 헬퍼 함수 (suspend transform 지원) + * transform 내부에서 suspend 함수를 호출할 수 있도록 지원 + */ +suspend inline fun safeApiCallWithSuspendTransform( + crossinline apiCall: suspend () -> ApiResponse, + crossinline transform: suspend (T) -> R +): Result { + return try { + val response = apiCall() + if (response.code == "SUCCESS") { + Result.success(transform(response.data)) + } else { + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Result.failure(e) + } +} + /** * 의미있는 데이터를 반환하지 않는 API 응답을 처리하는 전용 확장 함수 */ From 8292fc763f3638721ea3a5e42de1508b1d0ea562 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 22:05:32 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20=EC=97=B0=EB=8F=99=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/layout/fragment_account.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 1f34ff2a..55c252b4 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -76,7 +76,11 @@ android:layout_height="34dp" android:insetTop="0dp" android:insetBottom="0dp" + app:iconTint="@null" + app:iconPadding="4dp" + app:iconGravity="textStart" android:text="연동하기" + android:gravity="center" android:layout_marginEnd="16dp"/> From df354c5baca6ae7ee1d6c26fbb91be586ad4de49 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 22:26:31 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/api/AccountApiService.kt | 7 +++++++ .../app/data/model/account/LinkRequest.kt | 10 ++++++++++ .../app/ui/login/viewmodel/LoginViewModel.kt | 19 ++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/data/model/account/LinkRequest.kt diff --git a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt index 7a51d20a..27795fb6 100644 --- a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt @@ -4,9 +4,16 @@ import com.egobook.app.data.model.ApiResponse import com.egobook.app.data.model.account.AccountResponse import retrofit2.http.GET import retrofit2.Response +import retrofit2.http.POST interface AccountApiService { //유저 id 불러오기 @GET("/home/settings") suspend fun getUserId(): ApiResponse + + @POST("/users/link/google") + suspend fun linkToGoogle( + + ): ApiResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/account/LinkRequest.kt b/app/src/main/java/com/egobook/app/data/model/account/LinkRequest.kt new file mode 100644 index 00000000..efc680bf --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/account/LinkRequest.kt @@ -0,0 +1,10 @@ +package com.egobook.app.data.model.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LinkRequest( + @SerialName("idToken") + val idToken: String +) diff --git a/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt index 7a0afb3f..61a07c5d 100644 --- a/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt @@ -55,14 +55,27 @@ class LoginViewModel @Inject constructor( ) } } - //구글 로그인 시도 -> 토큰 재발급 + //구글 로그인 시도 -> 토큰 재발급 & 로그인 타입 저장 is LoginEvent.TryLoginByGoogle -> { viewModelScope.launch { _loginState.value = LoginState.Loading + val result = authUseCases.googleLogin(event.idToken) + result.fold( - onSuccess = { _loginState.value = LoginState.Success }, - onFailure = { error -> _loginState.value = LoginState.Error(error.message ?: "알 수 없는 오류") } + onSuccess = { + _loginState.value = LoginState.Success + + //구글 로그인 성공 시에도 로그인 타입 저장 + val loginType = UserInfoStorage.LoginType.GOOGLE + userInfoStorage.saveLoginType(loginType) + + Timber.d("구글 로그인 성공, loginType=$loginType") + }, + onFailure = { error -> + _loginState.value = + LoginState.Error(error.message ?: "알 수 없는 오류") + } ) } } From 1a12bd37a1eaa8b71ece21bfef520a245e4b6630 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 22:30:51 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20apiservice=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/egobook/app/data/api/AccountApiService.kt | 7 +++++-- .../egobook/app/data/model/account/LinkResponse.kt | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt diff --git a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt index 27795fb6..105b4def 100644 --- a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt @@ -2,8 +2,11 @@ package com.egobook.app.data.api import com.egobook.app.data.model.ApiResponse import com.egobook.app.data.model.account.AccountResponse +import com.egobook.app.data.model.account.LinkRequest +import com.egobook.app.data.model.account.LinkResponse import retrofit2.http.GET import retrofit2.Response +import retrofit2.http.Body import retrofit2.http.POST interface AccountApiService { @@ -13,7 +16,7 @@ interface AccountApiService { @POST("/users/link/google") suspend fun linkToGoogle( - - ): ApiResponse + @Body request: LinkRequest + ): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt b/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt new file mode 100644 index 00000000..0ff06f6f --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt @@ -0,0 +1,11 @@ +package com.egobook.app.data.model.account + +import kotlinx.serialization.SerialName + +data class LinkResponse( + @SerialName("accessToken") + val accessToken: String, + + @SerialName("refreshToken") + val refreshToken: String, +) From 5ad1ee98422de07a4989dc0fb7572db3e662edf3 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 23:03:55 +0900 Subject: [PATCH 09/22] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=97=B0=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/AccountRepositoryImpl.kt | 32 +++++++++++++++++++ .../repository/account/AccountRepository.kt | 7 ++++ .../ui/account/viewmodel/AccountViewModel.kt | 24 ++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index bd3bc4b8..55e8b74b 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -2,7 +2,9 @@ package com.egobook.app.data.repository.account import com.egobook.app.data.api.AccountApiService import com.egobook.app.data.local.UserInfoStorage +import com.egobook.app.data.model.account.LinkRequest import com.egobook.app.data.util.safeApiCall +import com.egobook.app.data.util.safeApiCallWithSuspendTransform import com.egobook.app.domain.repository.account.AccountRepository import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull @@ -36,4 +38,34 @@ class AccountRepositoryImpl @Inject constructor( return result } + override suspend fun linkToGoogle(idToken: String): Result { + // 현재 로그인 타입이 GUEST인지 체크 + val currentLoginType = userInfoStorage.getLoginType().firstOrNull() + + // GUEST가 아니면 에러 반환 + if (currentLoginType != UserInfoStorage.LoginType.GUEST) { + return Result.failure(Exception("게스트 계정만 구글 연동이 가능합니다.")) + } + + // idToken 전송 (API 호출) 및 토큰 저장 + return safeApiCallWithSuspendTransform( + apiCall = { + apiService.linkToGoogle(LinkRequest(idToken = idToken)) + }, + transform = { tokenData -> + // Access, Refresh Token 저장 (Recover Token은 null로 설정하여 저장하지 않음) + userInfoStorage.saveAllTokens( + accessToken = tokenData.accessToken, + refreshToken = tokenData.refreshToken, + recoverToken = null + ) + + // 4. 로그인 타입을 GOOGLE로 변경 + userInfoStorage.saveLoginType(UserInfoStorage.LoginType.GOOGLE) + + Unit + } + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt index 2709a56e..03fb9944 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt @@ -6,4 +6,11 @@ interface AccountRepository { * 유저 id 조회 */ suspend fun getUserId(): Result + + /** + * 구글 계정 연동 + */ + suspend fun linkToGoogle(idToken: String): Result + + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index 2f6cdb3a..cad631f3 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -5,9 +5,7 @@ import androidx.lifecycle.viewModelScope import com.egobook.app.domain.repository.account.AccountRepository import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,10 +17,13 @@ class AccountViewModel @Inject constructor( private val _userIdState = MutableStateFlow>(UiState.Idle) val userIdState = _userIdState.asStateFlow() + private val _linkState = MutableStateFlow>(UiState.Idle) + val linkState = _linkState.asStateFlow() + + init { getUserId() } - fun getUserId() { viewModelScope.launch { _userIdState.value = UiState.Loading @@ -38,4 +39,21 @@ class AccountViewModel @Inject constructor( } } + fun linkToGoogle(idToken: String) { + viewModelScope.launch { + _linkState.value = UiState.Loading + + accountRepository.linkToGoogle(idToken) + .onSuccess { + _linkState.value = UiState.Success(Unit) + } + .onFailure { e -> + _linkState.value = + UiState.Failure(e.message ?: "구글 계정 연동에 실패했습니다") + } + } + + } + + } From 20ce626a1c3d2508bb0230c730050a5a7c93dd4d Mon Sep 17 00:00:00 2001 From: princehw03 Date: Tue, 10 Feb 2026 23:51:52 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20api=201=EC=B0=A8=20=EC=97=B0=EB=8F=99=EC=99=84?= =?UTF-8?q?=EB=A3=8C.=20=EB=B3=B4=EC=99=84=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/AccountBottomSheetFragment.kt | 10 +- .../app/ui/account/view/AccountFragment.kt | 101 +++++++++++++++++- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt index 1d10769e..f147d1cd 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt @@ -26,8 +26,6 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { } - - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -38,6 +36,14 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setClickListener() + } + + private fun setClickListener() { + binding.btnBottomGoogleLogin.setOnClickListener { + linkConfirmListener?.onLinkConfirmed() + dismiss() + } } override fun onDismiss(dialog: DialogInterface) { diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index ec8e1ade..05cc241b 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -5,26 +5,42 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.GetCredentialException import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import com.egobook.app.BuildConfig import com.egobook.app.R import com.egobook.app.databinding.FragmentAccountBinding import com.egobook.app.ui.account.viewmodel.AccountViewModel import com.egobook.app.util.UiState +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import timber.log.Timber @AndroidEntryPoint class AccountFragment : Fragment() { private var _binding: FragmentAccountBinding? = null private val binding get() = _binding!! - private val viewModel: AccountViewModel by viewModels() + + private lateinit var request: GetCredentialRequest + + private val credentialManager by lazy { + CredentialManager.create(requireContext()) + } + private val blurRadius = 5f override fun onCreateView( @@ -40,6 +56,7 @@ class AccountFragment : Fragment() { setClickListeners() setupBlur() observeUserIdState() + observeLinkState() } private fun observeUserIdState() { @@ -51,6 +68,7 @@ class AccountFragment : Fragment() { is UiState.Idle -> Unit is UiState.Loading -> { + //추후 로딩뷰를 삽입하자 } is UiState.Success -> { @@ -66,6 +84,33 @@ class AccountFragment : Fragment() { } } + private fun observeLinkState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.linkState.collect { state -> + when (state) { + is UiState.Idle -> Unit + + is UiState.Loading -> { + //추후 로딩뷰를 삽입하자 + } + is UiState.Success -> { + binding.btnIntegrate.apply { + icon = ContextCompat.getDrawable(context, R.drawable.ic_google_logo) + text = "연동 완료" + isEnabled = false + } + Toast.makeText(requireContext(), "GOOGLE 계정 연동이 완료되었습니다!", Toast.LENGTH_SHORT).show() + } + is UiState.Failure -> { + + } + } + } + } + } + } + private fun setClickListeners() { binding.apply { btnBack.setOnClickListener { @@ -74,12 +119,62 @@ class AccountFragment : Fragment() { btnIntegrate.setOnClickListener { binding.blurView.visibility = View.VISIBLE - AccountBottomSheetFragment() - .show(childFragmentManager, AccountBottomSheetFragment.TAG) + val accountBottomSheetFragment = AccountBottomSheetFragment() + accountBottomSheetFragment.setOnLinkConfirmListener(object: AccountBottomSheetFragment.OnLinkConfirmListener { + override fun onLinkConfirmed() { + request = getGoogleRequest() + + lifecycleScope.launch{ + try { + val result = credentialManager.getCredential( + request = request, + context = requireContext() + ) + handleGoogleResult(result) + } catch (e: GetCredentialException) { + Timber.d("계정연동 실패: ${e.message}") + } + } + } + }) + accountBottomSheetFragment.show(childFragmentManager, AccountBottomSheetFragment.TAG) } } } + private fun getGoogleRequest(): GetCredentialRequest { + val googleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) // 모든 구글 계정 표시 + .setServerClientId(BuildConfig.GOOGLE_WEB_CLIENT_ID) + .setAutoSelectEnabled(false) // 사용자가 직접 선택 + .build() + + return GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + } + + private fun handleGoogleResult(result: GetCredentialResponse) { + val credential = result.credential + + if (credential is CustomCredential && + credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL + ) { + try { + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + val idToken = googleIdTokenCredential.idToken + Timber.d("Google ID Token 받음") + + //뷰모델의 linkToGoogle 메서드 호출 + viewModel.linkToGoogle(idToken) + } catch (e: GetCredentialException) { + Timber.e(e, "구글 토큰 파싱 실패") + } + } else { + Timber.e("구글 로그인 credential 아님") + } + } + override fun onDestroyView() { super.onDestroyView() From 2eec0eaa452789e8fc352614cb7161970381f4f7 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 00:28:17 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor:=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80id=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20api=20=ED=98=B8=EC=B6=9C=20=EA=B0=95?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repository/account/AccountRepositoryImpl.kt | 9 ++++----- .../domain/repository/account/AccountRepository.kt | 2 +- .../app/ui/account/viewmodel/AccountViewModel.kt | 11 ++++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index 55e8b74b..a6d701cb 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -14,13 +14,13 @@ class AccountRepositoryImpl @Inject constructor( private val userInfoStorage: UserInfoStorage ) : AccountRepository { - override suspend fun getUserId(): Result { + override suspend fun getUserId(forceRefresh: Boolean): Result { //datastore에 데이터가 있는지 먼저 체크 val localUserId = userInfoStorage.getUserId().firstOrNull() - //있다면 바로 리턴 - if (!localUserId.isNullOrBlank()) { + //datastore에 데이터가 있고 갱신을 강제하지 않는다면 바로 리턴 + if (!localUserId.isNullOrBlank() && !forceRefresh) { return Result.success(localUserId) } @@ -60,9 +60,8 @@ class AccountRepositoryImpl @Inject constructor( recoverToken = null ) - // 4. 로그인 타입을 GOOGLE로 변경 + // 로그인 타입을 GOOGLE로 변경 userInfoStorage.saveLoginType(UserInfoStorage.LoginType.GOOGLE) - Unit } ) diff --git a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt index 03fb9944..91cf692a 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt @@ -5,7 +5,7 @@ interface AccountRepository { /** * 유저 id 조회 */ - suspend fun getUserId(): Result + suspend fun getUserId(forceRefresh: Boolean = false): Result /** * 구글 계정 연동 diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index cad631f3..4df18e2e 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -46,6 +46,16 @@ class AccountViewModel @Inject constructor( accountRepository.linkToGoogle(idToken) .onSuccess { _linkState.value = UiState.Success(Unit) + + // 연동 성공 후 userId 갱신 + accountRepository.getUserId(forceRefresh = true) + .onSuccess { id -> + _userIdState.value = UiState.Success(id) + } + .onFailure { e -> + _userIdState.value = + UiState.Failure(e.message ?: "사용자 ID를 새로 불러오지 못했습니다") + } } .onFailure { e -> _linkState.value = @@ -55,5 +65,4 @@ class AccountViewModel @Inject constructor( } - } From b3e12e2f5bd1cf6325b7839760b3856657be2031 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 01:46:59 +0900 Subject: [PATCH 12/22] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=20=EC=97=B0=EB=8F=99=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=98=20=EC=83=81=ED=83=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/AccountBottomSheetFragment.kt | 17 ++++++--- .../app/ui/account/view/AccountFragment.kt | 35 +++++++++++++++---- .../ui/account/viewmodel/AccountViewModel.kt | 15 +++++++- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt index f147d1cd..f204b8af 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.egobook.app.databinding.FragmentAccountBottomSheetBinding -import com.egobook.app.ui.account.view.AccountFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment class AccountBottomSheetFragment : BottomSheetDialogFragment() { @@ -14,6 +13,8 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { private var _binding: FragmentAccountBottomSheetBinding? = null private val binding get() = _binding!! + var isLinked: Boolean = false // 연동 여부 상태 + //연동 확인 콜백 인터페이스 interface OnLinkConfirmListener { fun onLinkConfirmed() @@ -40,9 +41,17 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { } private fun setClickListener() { - binding.btnBottomGoogleLogin.setOnClickListener { - linkConfirmListener?.onLinkConfirmed() - dismiss() + binding.btnBottomGoogleLogin.apply { + if (isLinked) { + text = "Google계정으로 연동되었습니다" + isEnabled = false + } + + setOnClickListener { + if (!isLinked) { + linkConfirmListener?.onLinkConfirmed() + } + } } } diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 05cc241b..e875bb9f 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -27,16 +27,15 @@ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import timber.log.Timber +import android.util.Base64 +import org.json.JSONObject @AndroidEntryPoint class AccountFragment : Fragment() { - private var _binding: FragmentAccountBinding? = null private val binding get() = _binding!! private val viewModel: AccountViewModel by viewModels() - private lateinit var request: GetCredentialRequest - private val credentialManager by lazy { CredentialManager.create(requireContext()) } @@ -84,6 +83,9 @@ class AccountFragment : Fragment() { } } + private val Int.dp: Int + get() = (this * resources.displayMetrics.density).toInt() + private fun observeLinkState() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -96,11 +98,15 @@ class AccountFragment : Fragment() { } is UiState.Success -> { binding.btnIntegrate.apply { + layoutParams = layoutParams.apply { + width = 111.dp // 111dp + height = 34.dp // 34dp + } + icon = ContextCompat.getDrawable(context, R.drawable.ic_google_logo) - text = "연동 완료" - isEnabled = false + text = "연동완료" + //isEnabled = false } - Toast.makeText(requireContext(), "GOOGLE 계정 연동이 완료되었습니다!", Toast.LENGTH_SHORT).show() } is UiState.Failure -> { @@ -119,7 +125,12 @@ class AccountFragment : Fragment() { btnIntegrate.setOnClickListener { binding.blurView.visibility = View.VISIBLE + val accountBottomSheetFragment = AccountBottomSheetFragment() + + // 현재 연동 상태 전달 + accountBottomSheetFragment.isLinked = viewModel.linkState.value is UiState.Success + accountBottomSheetFragment.setOnLinkConfirmListener(object: AccountBottomSheetFragment.OnLinkConfirmListener { override fun onLinkConfirmed() { request = getGoogleRequest() @@ -167,6 +178,7 @@ class AccountFragment : Fragment() { //뷰모델의 linkToGoogle 메서드 호출 viewModel.linkToGoogle(idToken) + Toast.makeText(requireContext(), "GOOGLE 계정 연동이 완료되었습니다!", Toast.LENGTH_SHORT).show() } catch (e: GetCredentialException) { Timber.e(e, "구글 토큰 파싱 실패") } @@ -175,13 +187,22 @@ class AccountFragment : Fragment() { } } + fun parseEmailFromIdToken(idToken: String): String? { + // ID 토큰은 "header.payload.signature" 형태 + val parts = idToken.split(".") + if (parts.size != 3) return null + + val payload = parts[1] + val decodedBytes = Base64.decode(payload, Base64.URL_SAFE) + val payloadJson = JSONObject(String(decodedBytes)) + return payloadJson.optString("email") + } override fun onDestroyView() { super.onDestroyView() _binding = null } - //=============다이알로그 출력용 블러뷰 세팅==================== private fun setupBlur() { diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index 4df18e2e..76494d42 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -1,18 +1,23 @@ package com.egobook.app.ui.account.viewmodel +import android.widget.Toast +import androidx.core.content.ContentProviderCompat.requireContext import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.egobook.app.data.local.UserInfoStorage import com.egobook.app.domain.repository.account.AccountRepository import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class AccountViewModel @Inject constructor( - private val accountRepository: AccountRepository + private val accountRepository: AccountRepository, + private val userInfoStorage: UserInfoStorage ) : ViewModel() { private val _userIdState = MutableStateFlow>(UiState.Idle) val userIdState = _userIdState.asStateFlow() @@ -22,6 +27,14 @@ class AccountViewModel @Inject constructor( init { + //로그인 타입을 확인하여 연동 가능 여부를 판단 + viewModelScope.launch { + val loginType = userInfoStorage.getLoginType().firstOrNull() + if (loginType == UserInfoStorage.LoginType.GOOGLE) { + _linkState.value = UiState.Success(Unit) + } + } + getUserId() } fun getUserId() { From b018a1f62b38a841bc68c38b15252359375c7cf1 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 01:53:46 +0900 Subject: [PATCH 13/22] =?UTF-8?q?feat:=20id=ED=86=A0=ED=81=B0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=EB=B6=80=ED=84=B0=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=8B=B1?= =?UTF-8?q?=EA=B8=80=ED=86=A4=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/account/view/AccountFragment.kt | 2 ++ .../egobook/app/ui/util/GoogleTokenUtils.kt | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index e875bb9f..4ece36a1 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -174,6 +174,8 @@ class AccountFragment : Fragment() { try { val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) val idToken = googleIdTokenCredential.idToken + val email = parseEmailFromIdToken(idToken) + Timber.d("Google 이메일: $email") Timber.d("Google ID Token 받음") //뷰모델의 linkToGoogle 메서드 호출 diff --git a/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt b/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt new file mode 100644 index 00000000..8479c1ad --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt @@ -0,0 +1,28 @@ +package com.egobook.app.ui.util + +import android.util.Base64 +import org.json.JSONObject + +object GoogleTokenUtils { + + /** + * ID 토큰에서 이메일 추출 + * @param idToken 구글 로그인/회원가입 시 받은 ID 토큰 + * @return 이메일, 없으면 null + */ + fun parseEmailFromIdToken(idToken: String): String? { + try { + // ID 토큰은 "header.payload.signature" 형태 + val parts = idToken.split(".") + if (parts.size != 3) return null + + val payload = parts[1] + val decodedBytes = Base64.decode(payload, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) + val payloadJson = JSONObject(String(decodedBytes)) + return payloadJson.optString("email") + } catch (e: Exception) { + e.printStackTrace() + return null + } + } +} \ No newline at end of file From a3d4f0a4506ed165f488247f2ba80534c5681246 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 02:11:13 +0900 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EA=B0=9D=EC=B2=B4=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?->=20=EB=B0=B1=EC=97=94=EB=93=9C=EC=97=90=EC=84=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=EA=B2=83=EC=9D=B4=20=EC=95=84=ED=82=A4=ED=85=8D=EC=B3=90=20?= =?UTF-8?q?=EC=83=81=20=EB=A7=9E=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/account/view/AccountFragment.kt | 13 --------- .../egobook/app/ui/util/GoogleTokenUtils.kt | 28 ------------------- 2 files changed, 41 deletions(-) delete mode 100644 app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 4ece36a1..7fd6afd9 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -174,8 +174,6 @@ class AccountFragment : Fragment() { try { val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) val idToken = googleIdTokenCredential.idToken - val email = parseEmailFromIdToken(idToken) - Timber.d("Google 이메일: $email") Timber.d("Google ID Token 받음") //뷰모델의 linkToGoogle 메서드 호출 @@ -189,17 +187,6 @@ class AccountFragment : Fragment() { } } - fun parseEmailFromIdToken(idToken: String): String? { - // ID 토큰은 "header.payload.signature" 형태 - val parts = idToken.split(".") - if (parts.size != 3) return null - - val payload = parts[1] - val decodedBytes = Base64.decode(payload, Base64.URL_SAFE) - val payloadJson = JSONObject(String(decodedBytes)) - return payloadJson.optString("email") - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt b/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt deleted file mode 100644 index 8479c1ad..00000000 --- a/app/src/main/java/com/egobook/app/ui/util/GoogleTokenUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.egobook.app.ui.util - -import android.util.Base64 -import org.json.JSONObject - -object GoogleTokenUtils { - - /** - * ID 토큰에서 이메일 추출 - * @param idToken 구글 로그인/회원가입 시 받은 ID 토큰 - * @return 이메일, 없으면 null - */ - fun parseEmailFromIdToken(idToken: String): String? { - try { - // ID 토큰은 "header.payload.signature" 형태 - val parts = idToken.split(".") - if (parts.size != 3) return null - - val payload = parts[1] - val decodedBytes = Base64.decode(payload, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) - val payloadJson = JSONObject(String(decodedBytes)) - return payloadJson.optString("email") - } catch (e: Exception) { - e.printStackTrace() - return null - } - } -} \ No newline at end of file From 5f33fd0cf7a0af0dfd251d24ae573f58cf38bb8b Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 15:39:43 +0900 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20api?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/local/UserInfoStorage.kt | 21 +++++++- .../app/data/model/account/LinkResponse.kt | 3 ++ .../app/data/model/auth/AuthResponse.kt | 5 +- .../account/AccountRepositoryImpl.kt | 9 +++- .../repository/auth/AuthRepositoryImpl.kt | 12 ++++- .../view/AccountBottomSheetFragment.kt | 50 +++++++++++++++---- .../app/ui/account/view/AccountFragment.kt | 16 ++++-- .../ui/account/viewmodel/AccountViewModel.kt | 12 ++++- .../app/ui/login/viewmodel/LoginViewModel.kt | 5 -- 9 files changed, 109 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt index ea709e6b..c9003ff8 100644 --- a/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt +++ b/app/src/main/java/com/egobook/app/data/local/UserInfoStorage.kt @@ -22,7 +22,6 @@ class UserInfoStorage @Inject constructor( ) { private val dataStore = context.dataStore - /** * LoginType 저장 */ @@ -66,6 +65,24 @@ class UserInfoStorage @Inject constructor( } } + /** + * USER EMAIL 저장 + */ + suspend fun saveUserEmail(email: String) { + dataStore.edit { preferences -> + preferences[USER_EMAIL] = email + } + } + + /** + * USER EMAIL 읽기 + */ + fun getUserEmail(): Flow { + return dataStore.data.map { preferences -> + preferences[USER_EMAIL] + } + } + /** * Access Token 저장 */ @@ -172,6 +189,8 @@ class UserInfoStorage @Inject constructor( private val LOGIN_TYPE = stringPreferencesKey("login_type") private val USER_ID = stringPreferencesKey("user_id") + + private val USER_EMAIL = stringPreferencesKey("email") private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") private val KEY_REFRESH_TOKEN = stringPreferencesKey("refresh_token") private val KEY_RECOVER_TOKEN = stringPreferencesKey("recover_token") diff --git a/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt b/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt index 0ff06f6f..fa909b34 100644 --- a/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/account/LinkResponse.kt @@ -8,4 +8,7 @@ data class LinkResponse( @SerialName("refreshToken") val refreshToken: String, + + @SerialName("email") + val email: String, ) diff --git a/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt b/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt index 9d1cc3d6..71b8c486 100644 --- a/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt +++ b/app/src/main/java/com/egobook/app/data/model/auth/AuthResponse.kt @@ -13,7 +13,10 @@ data class TokenData( val accessToken: String, @SerialName("refreshToken") - val refreshToken: String + val refreshToken: String, + + @SerialName("email") + val email: String, ) /** diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index a6d701cb..8f814b49 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -8,6 +8,7 @@ import com.egobook.app.data.util.safeApiCallWithSuspendTransform import com.egobook.app.domain.repository.account.AccountRepository import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull +import timber.log.Timber class AccountRepositoryImpl @Inject constructor( private val apiService: AccountApiService, @@ -61,7 +62,13 @@ class AccountRepositoryImpl @Inject constructor( ) // 로그인 타입을 GOOGLE로 변경 - userInfoStorage.saveLoginType(UserInfoStorage.LoginType.GOOGLE) + val loginType = UserInfoStorage.LoginType.GOOGLE + userInfoStorage.saveLoginType(loginType) + + // 이메일 저장 + userInfoStorage.saveUserEmail(tokenData.email) + Timber.d("구글 로그인 성공, loginType=$loginType, email=${tokenData.email}") + Unit } ) diff --git a/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt index 7aa72399..f92dcce2 100644 --- a/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/auth/AuthRepositoryImpl.kt @@ -33,7 +33,10 @@ class AuthRepositoryImpl @Inject constructor( ) val loginType = UserInfoStorage.LoginType.GOOGLE userInfoStorage.saveLoginType(loginType) - Timber.d("구글 회원가입 성공, loginType=$loginType") + + //유저 이메일 저장 + userInfoStorage.saveUserEmail(tokenData.email) + Timber.d("구글 로그인 성공, loginType=$loginType, email=${tokenData.email}") Unit } ) @@ -109,6 +112,7 @@ class AuthRepositoryImpl @Inject constructor( } } + //구글 로그인 시 사용 override suspend fun refreshTokens(idToken: String): Result { // 액세스 토큰 가져오기 (없으면 null) val accessToken = userInfoStorage.getAccessToken().first() @@ -127,6 +131,12 @@ class AuthRepositoryImpl @Inject constructor( accessToken = tokenData.accessToken, refreshToken = tokenData.refreshToken ) + //로그인 타입 저장 + val loginType = UserInfoStorage.LoginType.GOOGLE + userInfoStorage.saveLoginType(loginType) + //유저 이메일 저장 + userInfoStorage.saveUserEmail(tokenData.email) + Timber.d("구글 로그인 성공, loginType=$loginType, email=${tokenData.email}") Unit } ) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt index f204b8af..70d4940d 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountBottomSheetFragment.kt @@ -5,15 +5,25 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.egobook.app.databinding.FragmentAccountBottomSheetBinding +import com.egobook.app.ui.account.viewmodel.AccountViewModel +import com.egobook.app.util.UiState import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch class AccountBottomSheetFragment : BottomSheetDialogFragment() { private var _binding: FragmentAccountBottomSheetBinding? = null private val binding get() = _binding!! - var isLinked: Boolean = false // 연동 여부 상태 + //부모 프래그먼트의 뷰모델 공유 + private val viewModel: AccountViewModel by viewModels({ requireParentFragment() }) + //연동 확인 콜백 인터페이스 interface OnLinkConfirmListener { @@ -38,23 +48,43 @@ class AccountBottomSheetFragment : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setClickListener() + observeLinkState() } - private fun setClickListener() { - binding.btnBottomGoogleLogin.apply { - if (isLinked) { - text = "Google계정으로 연동되었습니다" - isEnabled = false - } + private fun observeLinkState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - setOnClickListener { - if (!isLinked) { - linkConfirmListener?.onLinkConfirmed() + // 연동 상태 및 이메일 관찰 + launch { + viewModel.linkState.collect { state -> + val isLinked = state is UiState.Success + if (isLinked) { + binding.tvAccountEmail.apply { + visibility = View.VISIBLE + text = viewModel.userEmail.firstOrNull() ?: "" + } + binding.btnBottomGoogleLogin.apply { + // 이메일이 있으면 표시, 없으면 기본 메시지 + text = "Google계정으로 연동되었습니다" + isEnabled = false + } + } + } } } } } + private fun setClickListener() { + binding.btnBottomGoogleLogin.setOnClickListener { + // linkState가 Success가 아닐 때만 클릭 가능 + if (viewModel.linkState.value !is UiState.Success) { + linkConfirmListener?.onLinkConfirmed() + } + } + } + override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) (parentFragment as? AccountFragment)?.clearBlur() diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 7fd6afd9..44d05e8a 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import timber.log.Timber import android.util.Base64 +import kotlinx.coroutines.flow.MutableSharedFlow import org.json.JSONObject @AndroidEntryPoint @@ -56,6 +57,7 @@ class AccountFragment : Fragment() { setupBlur() observeUserIdState() observeLinkState() + observeLinkToastEvent() } private fun observeUserIdState() { @@ -117,6 +119,16 @@ class AccountFragment : Fragment() { } } + private fun observeLinkToastEvent() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.linkToastEvent.collect { message -> + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + } + } + } + private fun setClickListeners() { binding.apply { btnBack.setOnClickListener { @@ -128,9 +140,6 @@ class AccountFragment : Fragment() { val accountBottomSheetFragment = AccountBottomSheetFragment() - // 현재 연동 상태 전달 - accountBottomSheetFragment.isLinked = viewModel.linkState.value is UiState.Success - accountBottomSheetFragment.setOnLinkConfirmListener(object: AccountBottomSheetFragment.OnLinkConfirmListener { override fun onLinkConfirmed() { request = getGoogleRequest() @@ -178,7 +187,6 @@ class AccountFragment : Fragment() { //뷰모델의 linkToGoogle 메서드 호출 viewModel.linkToGoogle(idToken) - Toast.makeText(requireContext(), "GOOGLE 계정 연동이 완료되었습니다!", Toast.LENGTH_SHORT).show() } catch (e: GetCredentialException) { Timber.e(e, "구글 토큰 파싱 실패") } diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index 76494d42..1a77b29f 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -8,7 +8,9 @@ import com.egobook.app.data.local.UserInfoStorage import com.egobook.app.domain.repository.account.AccountRepository import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -25,9 +27,14 @@ class AccountViewModel @Inject constructor( private val _linkState = MutableStateFlow>(UiState.Idle) val linkState = _linkState.asStateFlow() + private val _linkToastEvent = MutableSharedFlow(replay = 1) + val linkToastEvent = _linkToastEvent.asSharedFlow() + + // UserInfoStorage에서 직접 이메일을 읽어옴 + val userEmail = userInfoStorage.getUserEmail() init { - //로그인 타입을 확인하여 연동 가능 여부를 판단 + //로그인 타입을 확인하여 연동 상태 초기화 viewModelScope.launch { val loginType = userInfoStorage.getLoginType().firstOrNull() if (loginType == UserInfoStorage.LoginType.GOOGLE) { @@ -59,6 +66,7 @@ class AccountViewModel @Inject constructor( accountRepository.linkToGoogle(idToken) .onSuccess { _linkState.value = UiState.Success(Unit) + _linkToastEvent.emit("Google 계정 연동이 완료되었습니다!") // 연동 성공 후 userId 갱신 accountRepository.getUserId(forceRefresh = true) @@ -68,11 +76,13 @@ class AccountViewModel @Inject constructor( .onFailure { e -> _userIdState.value = UiState.Failure(e.message ?: "사용자 ID를 새로 불러오지 못했습니다") + _linkToastEvent.emit(e.message ?: "구글 계정 연동 실패") } } .onFailure { e -> _linkState.value = UiState.Failure(e.message ?: "구글 계정 연동에 실패했습니다") + _linkToastEvent.emit("구글 계정 연동에 실패했습니다.") } } diff --git a/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt index 61a07c5d..0f4fc102 100644 --- a/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/login/viewmodel/LoginViewModel.kt @@ -66,11 +66,6 @@ class LoginViewModel @Inject constructor( onSuccess = { _loginState.value = LoginState.Success - //구글 로그인 성공 시에도 로그인 타입 저장 - val loginType = UserInfoStorage.LoginType.GOOGLE - userInfoStorage.saveLoginType(loginType) - - Timber.d("구글 로그인 성공, loginType=$loginType") }, onFailure = { error -> _loginState.value = From aaf0f04e0b427495fc381083a15281c0e8af2c00 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 16:02:36 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor:=20=EB=B7=B0=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20datastore=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EA=B7=B8=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/AccountRepositoryImpl.kt | 15 ++++++++++ .../repository/account/AccountRepository.kt | 11 ++++++- .../ui/account/viewmodel/AccountViewModel.kt | 30 +++++++++---------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index 8f814b49..8d43a086 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -6,6 +6,7 @@ import com.egobook.app.data.model.account.LinkRequest import com.egobook.app.data.util.safeApiCall import com.egobook.app.data.util.safeApiCallWithSuspendTransform import com.egobook.app.domain.repository.account.AccountRepository +import com.egobook.app.domain.repository.account.LinkedAccountInfo import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -74,4 +75,18 @@ class AccountRepositoryImpl @Inject constructor( ) } + override suspend fun getLinkedAccountInfo(): Result { + val loginType = userInfoStorage.getLoginType().firstOrNull() + val isGoogleLinked = loginType == UserInfoStorage.LoginType.GOOGLE + val email = userInfoStorage.getUserEmail().firstOrNull() + + return Result.success( + LinkedAccountInfo( + email = email, + isGoogleLinked = isGoogleLinked + ) + ) + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt index 91cf692a..8ef71edc 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt @@ -12,5 +12,14 @@ interface AccountRepository { */ suspend fun linkToGoogle(idToken: String): Result + /** + * 로컬에서 게스트타입과 GOOGLE이면 email을 읽어오는 로직 + */ + suspend fun getLinkedAccountInfo(): Result + +} -} \ No newline at end of file +data class LinkedAccountInfo( + val email: String?, + val isGoogleLinked: Boolean // loginType == GOOGLE +) \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index 1a77b29f..ada55fea 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -1,10 +1,7 @@ package com.egobook.app.ui.account.viewmodel -import android.widget.Toast -import androidx.core.content.ContentProviderCompat.requireContext import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.egobook.app.data.local.UserInfoStorage import com.egobook.app.domain.repository.account.AccountRepository import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,14 +9,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class AccountViewModel @Inject constructor( - private val accountRepository: AccountRepository, - private val userInfoStorage: UserInfoStorage + private val accountRepository: AccountRepository ) : ViewModel() { private val _userIdState = MutableStateFlow>(UiState.Idle) val userIdState = _userIdState.asStateFlow() @@ -30,19 +25,24 @@ class AccountViewModel @Inject constructor( private val _linkToastEvent = MutableSharedFlow(replay = 1) val linkToastEvent = _linkToastEvent.asSharedFlow() - // UserInfoStorage에서 직접 이메일을 읽어옴 - val userEmail = userInfoStorage.getUserEmail() + private val _userEmail = MutableStateFlow(null) + val userEmail = _userEmail.asStateFlow() init { - //로그인 타입을 확인하여 연동 상태 초기화 + loadLinkedAccountInfo() + getUserId() + } + + private fun loadLinkedAccountInfo() { viewModelScope.launch { - val loginType = userInfoStorage.getLoginType().firstOrNull() - if (loginType == UserInfoStorage.LoginType.GOOGLE) { - _linkState.value = UiState.Success(Unit) - } + accountRepository.getLinkedAccountInfo() + .onSuccess { info -> + if (info.isGoogleLinked) { + _userEmail.value = info.email + _linkState.value = UiState.Success(Unit) + } + } } - - getUserId() } fun getUserId() { viewModelScope.launch { From 0e110d249e20af478c0366d87cc784f9ff2968ad Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 16:08:16 +0900 Subject: [PATCH 17/22] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=8B=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=8A=A4=ED=86=A0=EC=96=B4=EC=9D=98=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=81=B4=EB=A6=AC?= =?UTF-8?q?=EC=96=B4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/data/interceptor/TokenAuthenticator.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt b/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt index ea16dc1b..39117e9d 100644 --- a/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt +++ b/app/src/main/java/com/egobook/app/data/interceptor/TokenAuthenticator.kt @@ -95,17 +95,20 @@ class TokenAuthenticator @Inject constructor( } /** - * 로그아웃 처리: 로그인 화면으로 이동 + * 로그아웃 처리: 토큰 클리어 후 로그인 화면으로 이동 */ private fun handleLogout() { Timber.d("로그아웃 처리 시작") - - // 로그인 화면으로 이동 + + runBlocking { + userInfoStorage.clearAll() // 토큰, id, 이메일 등등 싹다 삭제 + } + val intent = Intent(context, LoginActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } context.startActivity(intent) - + Timber.d("로그인 화면으로 이동") } From b4689e0be5d0e97a31069f46159db69fb1ae0ee8 Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 17:32:42 +0900 Subject: [PATCH 18/22] =?UTF-8?q?design:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20dialog=20=EB=91=90=EA=B0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/data/api/AccountApiService.kt | 5 ++ .../model/account/DeleteAccountResponse.kt | 10 +++ .../account/AccountRepositoryImpl.kt | 24 ++++++ .../repository/account/AccountRepository.kt | 5 ++ .../view/AccountDeleteDialog1Fragment.kt | 52 ++++++++++++ .../view/AccountDeleteDialog2Fragment.kt | 37 ++++++++ .../app/ui/account/view/AccountFragment.kt | 10 +++ .../main/res/drawable/bg_delete_dialog.xml | 6 ++ app/src/main/res/layout/fragment_account.xml | 1 + .../fragment_account_delete_dialog1.xml | 85 +++++++++++++++++++ .../fragment_account_delete_dialog_2.xml | 46 ++++++++++ 11 files changed, 281 insertions(+) create mode 100644 app/src/main/java/com/egobook/app/data/model/account/DeleteAccountResponse.kt create mode 100644 app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt create mode 100644 app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt create mode 100644 app/src/main/res/drawable/bg_delete_dialog.xml create mode 100644 app/src/main/res/layout/fragment_account_delete_dialog1.xml create mode 100644 app/src/main/res/layout/fragment_account_delete_dialog_2.xml diff --git a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt index 105b4def..bbf53215 100644 --- a/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt +++ b/app/src/main/java/com/egobook/app/data/api/AccountApiService.kt @@ -2,11 +2,13 @@ package com.egobook.app.data.api import com.egobook.app.data.model.ApiResponse import com.egobook.app.data.model.account.AccountResponse +import com.egobook.app.data.model.account.DeleteAccountResponse import com.egobook.app.data.model.account.LinkRequest import com.egobook.app.data.model.account.LinkResponse import retrofit2.http.GET import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.POST interface AccountApiService { @@ -19,4 +21,7 @@ interface AccountApiService { @Body request: LinkRequest ): ApiResponse + @DELETE("/users/withdraw") + suspend fun deleteAccount(): ApiResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/data/model/account/DeleteAccountResponse.kt b/app/src/main/java/com/egobook/app/data/model/account/DeleteAccountResponse.kt new file mode 100644 index 00000000..8e59ccaf --- /dev/null +++ b/app/src/main/java/com/egobook/app/data/model/account/DeleteAccountResponse.kt @@ -0,0 +1,10 @@ +package com.egobook.app.data.model.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DeleteAccountResponse( + @SerialName("data") + val data: String, +) diff --git a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt index 8d43a086..beeb7843 100644 --- a/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt +++ b/app/src/main/java/com/egobook/app/data/repository/account/AccountRepositoryImpl.kt @@ -88,5 +88,29 @@ class AccountRepositoryImpl @Inject constructor( ) } + override suspend fun deleteAccount(): Result { + return try { + val result = safeApiCall( + apiCall = { apiService.deleteAccount() }, + transform = { Unit } + ) + + result.onSuccess { + try { + //datastore의 모든 데이터 삭제 + userInfoStorage.clearAll() + Timber.d("회원 탈퇴 성공: UserInfoStorage 초기화 완료") + } catch (e: Exception) { + Timber.e(e, "회원 탈퇴 후 UserInfoStorage 초기화 실패") + } + } + + result + } catch (e: Exception) { + Timber.e(e, "회원 탈퇴 처리 중 예외 발생") + Result.failure(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt index 8ef71edc..fa5ccbff 100644 --- a/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt +++ b/app/src/main/java/com/egobook/app/domain/repository/account/AccountRepository.kt @@ -17,6 +17,11 @@ interface AccountRepository { */ suspend fun getLinkedAccountInfo(): Result + /** + * 계정 탈퇴 + */ + suspend fun deleteAccount(): Result + } data class LinkedAccountInfo( diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt new file mode 100644 index 00000000..ce8ce902 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt @@ -0,0 +1,52 @@ +package com.egobook.app.ui.account.view + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.egobook.app.databinding.FragmentAccountDeleteDialog1Binding +import com.egobook.app.removeScreenBlur + +class AccountDeleteDialog1Fragment : DialogFragment() { + + private var _binding: FragmentAccountDeleteDialog1Binding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + _binding = FragmentAccountDeleteDialog1Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setClickListener() + } + + private fun setClickListener() { + binding.btnBack.setOnClickListener { + removeScreenBlur() + dismiss() + } + binding.btnRealDelete.setOnClickListener { + val accountDeleteDialog2Fragment = AccountDeleteDialog2Fragment() + accountDeleteDialog2Fragment.isCancelable = true + accountDeleteDialog2Fragment.show(parentFragmentManager, "AccountDeleteDialog2Fragment") + + removeScreenBlur() + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt new file mode 100644 index 00000000..cecd9427 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt @@ -0,0 +1,37 @@ +package com.egobook.app.ui.account.view + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import com.egobook.app.databinding.FragmentAccountDeleteDialog2Binding + +class AccountDeleteDialog2Fragment : DialogFragment() { + + private var _binding: FragmentAccountDeleteDialog2Binding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + _binding = FragmentAccountDeleteDialog2Binding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // TODO: 뷰 초기화 로직 추가 + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 44d05e8a..c859b7d1 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -28,6 +28,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import timber.log.Timber import android.util.Base64 +import com.egobook.app.BlurLevel +import com.egobook.app.applyScreenBlur import kotlinx.coroutines.flow.MutableSharedFlow import org.json.JSONObject @@ -159,6 +161,14 @@ class AccountFragment : Fragment() { }) accountBottomSheetFragment.show(childFragmentManager, AccountBottomSheetFragment.TAG) } + + tvDeleteAccount.setOnClickListener { + applyScreenBlur(BlurLevel.BASE) + + val accountDeleteDialog1Fragment = AccountDeleteDialog1Fragment() + accountDeleteDialog1Fragment.isCancelable = false + accountDeleteDialog1Fragment.show(childFragmentManager, "AccountDeleteDialog1Fragment") + } } } diff --git a/app/src/main/res/drawable/bg_delete_dialog.xml b/app/src/main/res/drawable/bg_delete_dialog.xml new file mode 100644 index 00000000..03835e9c --- /dev/null +++ b/app/src/main/res/drawable/bg_delete_dialog.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 55c252b4..391ecc7d 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -419,6 +419,7 @@ android:fontFamily="@font/arita_semibold"/> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_delete_dialog_2.xml b/app/src/main/res/layout/fragment_account_delete_dialog_2.xml new file mode 100644 index 00000000..c0a470d4 --- /dev/null +++ b/app/src/main/res/layout/fragment_account_delete_dialog_2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file From a0f8fcbd4e8e2de2f470257c4f3bc7b907c1e3fa Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 17:46:53 +0900 Subject: [PATCH 19/22] =?UTF-8?q?design:=20=EB=8B=A4=EC=9D=B4=EC=95=8C?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/account/view/AccountDeleteDialog1Fragment.kt | 1 - .../app/ui/account/view/AccountDeleteDialog2Fragment.kt | 7 +++++++ .../main/res/layout/fragment_account_delete_dialog_2.xml | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt index ce8ce902..002f69fd 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt @@ -40,7 +40,6 @@ class AccountDeleteDialog1Fragment : DialogFragment() { accountDeleteDialog2Fragment.isCancelable = true accountDeleteDialog2Fragment.show(parentFragmentManager, "AccountDeleteDialog2Fragment") - removeScreenBlur() dismiss() } } diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt index cecd9427..e6557db2 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt @@ -1,5 +1,6 @@ package com.egobook.app.ui.account.view +import android.content.DialogInterface import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle @@ -9,6 +10,7 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import com.egobook.app.databinding.FragmentAccountDeleteDialog2Binding +import com.egobook.app.removeScreenBlur class AccountDeleteDialog2Fragment : DialogFragment() { @@ -30,6 +32,11 @@ class AccountDeleteDialog2Fragment : DialogFragment() { // TODO: 뷰 초기화 로직 추가 } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + removeScreenBlur() + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/res/layout/fragment_account_delete_dialog_2.xml b/app/src/main/res/layout/fragment_account_delete_dialog_2.xml index c0a470d4..7d57bb91 100644 --- a/app/src/main/res/layout/fragment_account_delete_dialog_2.xml +++ b/app/src/main/res/layout/fragment_account_delete_dialog_2.xml @@ -19,7 +19,6 @@ android:paddingEnd="16dp" android:orientation="vertical" > Date: Wed, 11 Feb 2026 17:55:25 +0900 Subject: [PATCH 20/22] =?UTF-8?q?feat:=202=EB=B2=88=EC=A7=B8=20dialog?= =?UTF-8?q?=ED=9B=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/AccountDeleteDialog1Fragment.kt | 6 ++++++ .../view/AccountDeleteDialog2Fragment.kt | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt index 002f69fd..cad805b7 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt @@ -7,14 +7,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels import com.egobook.app.databinding.FragmentAccountDeleteDialog1Binding import com.egobook.app.removeScreenBlur +import com.egobook.app.ui.account.viewmodel.AccountViewModel +import kotlin.getValue class AccountDeleteDialog1Fragment : DialogFragment() { private var _binding: FragmentAccountDeleteDialog1Binding? = null private val binding get() = _binding!! + //부모 프래그먼트의 뷰모델 공유 + private val viewModel: AccountViewModel by viewModels({ requireParentFragment() }) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt index e6557db2..a7e6cfab 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog2Fragment.kt @@ -1,6 +1,7 @@ package com.egobook.app.ui.account.view import android.content.DialogInterface +import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle @@ -9,14 +10,23 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import com.egobook.app.databinding.FragmentAccountDeleteDialog2Binding import com.egobook.app.removeScreenBlur +import com.egobook.app.ui.account.viewmodel.AccountViewModel +import com.egobook.app.ui.login.view.LoginActivity +import timber.log.Timber +import kotlin.getValue class AccountDeleteDialog2Fragment : DialogFragment() { private var _binding: FragmentAccountDeleteDialog2Binding? = null private val binding get() = _binding!! + //부모 프래그먼트의 뷰모델 공유 + private val viewModel: AccountViewModel by viewModels({ requireParentFragment() }) + + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -32,8 +42,17 @@ class AccountDeleteDialog2Fragment : DialogFragment() { // TODO: 뷰 초기화 로직 추가 } + override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) + + //백스택 제거 후 로그인 화면으로 이동 + val intent = Intent(context, LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + Timber.d("로그인 화면으로 이동") + removeScreenBlur() } @@ -41,4 +60,6 @@ class AccountDeleteDialog2Fragment : DialogFragment() { super.onDestroyView() _binding = null } + + } From 0e47bb252d8a6f5c866422b37ab80db72f05cc9e Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 18:11:04 +0900 Subject: [PATCH 21/22] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20api=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/AccountDeleteDialog1Fragment.kt | 36 ++++++++++++++++--- .../ui/account/viewmodel/AccountViewModel.kt | 18 ++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt index cad805b7..96997ab8 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountDeleteDialog1Fragment.kt @@ -8,10 +8,14 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.egobook.app.databinding.FragmentAccountDeleteDialog1Binding import com.egobook.app.removeScreenBlur import com.egobook.app.ui.account.viewmodel.AccountViewModel -import kotlin.getValue +import com.egobook.app.util.UiState +import kotlinx.coroutines.launch class AccountDeleteDialog1Fragment : DialogFragment() { @@ -34,6 +38,7 @@ class AccountDeleteDialog1Fragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setClickListener() + observeDeleteAccountState() } private fun setClickListener() { @@ -42,14 +47,35 @@ class AccountDeleteDialog1Fragment : DialogFragment() { dismiss() } binding.btnRealDelete.setOnClickListener { - val accountDeleteDialog2Fragment = AccountDeleteDialog2Fragment() - accountDeleteDialog2Fragment.isCancelable = true - accountDeleteDialog2Fragment.show(parentFragmentManager, "AccountDeleteDialog2Fragment") + viewModel.deleteAccount() + } + } - dismiss() + private fun observeDeleteAccountState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.deleteAccountState.collect { state -> + when (state) { + is UiState.Success -> { + navigateToDeleteDialog2() + } + is UiState.Failure -> { + // TODO: 실패 처리 (토스트 메시지 등) + } + else -> {} + } + } + } } } + private fun navigateToDeleteDialog2() { + val accountDeleteDialog2Fragment = AccountDeleteDialog2Fragment() + accountDeleteDialog2Fragment.isCancelable = true + accountDeleteDialog2Fragment.show(parentFragmentManager, "AccountDeleteDialog2Fragment") + dismiss() + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index ada55fea..05e9937a 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -28,6 +28,10 @@ class AccountViewModel @Inject constructor( private val _userEmail = MutableStateFlow(null) val userEmail = _userEmail.asStateFlow() + private val _deleteAccountState = MutableStateFlow>(UiState.Idle) + val deleteAccountState = _deleteAccountState.asStateFlow() + + init { loadLinkedAccountInfo() getUserId() @@ -85,7 +89,21 @@ class AccountViewModel @Inject constructor( _linkToastEvent.emit("구글 계정 연동에 실패했습니다.") } } + } + + fun deleteAccount() { + viewModelScope.launch { + _deleteAccountState.value = UiState.Loading + accountRepository.deleteAccount() + .onSuccess { + _deleteAccountState.value = UiState.Success(Unit) + } + .onFailure { e -> + _deleteAccountState.value = + UiState.Failure(e.message ?: "회원 탈퇴에 실패했습니다") + } + } } } From ca7bcb86ab6f48cefeda93e59a41c00287590f9b Mon Sep 17 00:00:00 2001 From: princehw03 Date: Wed, 11 Feb 2026 18:22:48 +0900 Subject: [PATCH 22/22] =?UTF-8?q?design:=20=EA=B3=84=EC=A0=95=20=EC=B0=BD?= =?UTF-8?q?=20ui=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/egobook/app/MainActivity.kt | 3 ++- .../com/egobook/app/ui/account/view/AccountFragment.kt | 9 +++++++++ app/src/main/res/layout/fragment_account.xml | 8 +++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/egobook/app/MainActivity.kt b/app/src/main/java/com/egobook/app/MainActivity.kt index 78780218..82b0ac6e 100644 --- a/app/src/main/java/com/egobook/app/MainActivity.kt +++ b/app/src/main/java/com/egobook/app/MainActivity.kt @@ -33,9 +33,10 @@ class MainActivity : AppCompatActivity(), BlurController, NotificationController // 목적지 변경 리스너 추가: 특정 프래그먼트에서 바텀바 숨기기 navController.addOnDestinationChangedListener { _, destination, _ -> when (destination.id) { - R.id.diaryWriteFragment, // 일기 작성 화면 + R.id.diaryWriteFragment, // 일기 작성 화면 R.id.calenderFragment, // 달력 화면 R.id.storeFragment, + R.id.accountFragment //계정 화면 -> { binding.bottomNavigation.visibility = View.GONE } diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index c859b7d1..e7613770 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -28,6 +28,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import timber.log.Timber import android.util.Base64 +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import com.egobook.app.BlurLevel import com.egobook.app.applyScreenBlur import kotlinx.coroutines.flow.MutableSharedFlow @@ -55,6 +57,13 @@ class AccountFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, systemBars.bottom) + insets + } + setClickListeners() setupBlur() observeUserIdState() diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 391ecc7d..ce48ed7e 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -15,8 +15,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:fillViewport="true" - android:background="#F4F7EE" - android:paddingBottom="21dp"> + android:background="#F4F7EE"> - + +