From 6995ad7a74db2e2b67bd395fa117917c5ef308bd Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 24 Feb 2026 22:48:56 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat/#60:=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=B2=9C=20=EB=8B=A8=EC=9C=84=20=EC=BD=A4=EB=A7=88=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt index 63df189..1e7d41f 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt @@ -579,7 +579,7 @@ private fun FeedItemCard( createdAt = feed.createdAt, content = feed.content, productImageUrl = feed.productImageUrl, - price = feed.price, + price = String.format(java.util.Locale.KOREA, "%,d", feed.price), imageAspectRatio = feed.imageAspectRatio, isVoteEnded = feed.isVoteEnded, userVotedOptionIndex = feed.userVotedOptionIndex, From 48f0e97bfcf295e8f4602adf7a851102e9d5e2fc Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 24 Feb 2026 22:58:36 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat/#60:=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EC=8B=A0=EA=B3=A0/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/NotificationDetailContract.kt | 15 ++++- .../ui/NotificationDetailScreen.kt | 41 ++++++++++-- .../ui/NotificationDetailViewModel.kt | 66 ++++++++++++++++++- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt index 2903427..21bc126 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt @@ -1,6 +1,7 @@ package com.sseotdabwa.buyornot.feature.notification.ui import androidx.compose.runtime.Immutable +import com.sseotdabwa.buyornot.core.designsystem.icon.IconResource import com.sseotdabwa.buyornot.domain.model.Feed @Immutable @@ -8,10 +9,22 @@ data class NotificationDetailUiState( val isLoading: Boolean = true, val isError: Boolean = false, val feed: Feed? = null, + val isOwner: Boolean = false, ) sealed interface NotificationDetailIntent { data object OnRefresh : NotificationDetailIntent + + data object OnDeleteClicked : NotificationDetailIntent + + data object OnReportClicked : NotificationDetailIntent } -sealed interface NotificationDetailSideEffect +sealed interface NotificationDetailSideEffect { + data class ShowSnackbar( + val message: String, + val icon: IconResource? = null, + ) : NotificationDetailSideEffect + + data object NavigateBack : NotificationDetailSideEffect +} diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt index a2c1e8e..91224a5 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt @@ -1,6 +1,5 @@ package com.sseotdabwa.buyornot.feature.notification.ui -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -8,8 +7,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -19,8 +22,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sseotdabwa.buyornot.core.common.util.TimeUtils import com.sseotdabwa.buyornot.core.designsystem.components.BackTopBar import com.sseotdabwa.buyornot.core.designsystem.components.BuyOrNotErrorView +import com.sseotdabwa.buyornot.core.designsystem.components.BuyOrNotSnackBarHost import com.sseotdabwa.buyornot.core.designsystem.components.FeedCard import com.sseotdabwa.buyornot.core.designsystem.components.ImageAspectRatio +import com.sseotdabwa.buyornot.core.designsystem.components.showBuyOrNotSnackBar import com.sseotdabwa.buyornot.core.designsystem.theme.BuyOrNotTheme import com.sseotdabwa.buyornot.domain.model.Author import com.sseotdabwa.buyornot.domain.model.Feed @@ -43,9 +48,27 @@ fun NotificationDetailRoute( viewModel: NotificationDetailViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + + // SideEffect 처리 + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is NotificationDetailSideEffect.ShowSnackbar -> { + showBuyOrNotSnackBar( + snackbarHostState = snackbarHostState, + message = sideEffect.message, + iconResource = sideEffect.icon, + ) + } + NotificationDetailSideEffect.NavigateBack -> onBackClick() + } + } + } NotificationDetailScreen( uiState = uiState, + snackbarHostState = snackbarHostState, onBackClick = onBackClick, onIntent = viewModel::handleIntent, ) @@ -56,16 +79,19 @@ fun NotificationDetailScreen( uiState: NotificationDetailUiState, onBackClick: () -> Unit, onIntent: (NotificationDetailIntent) -> Unit, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ) { - Box(modifier = Modifier.fillMaxSize()) { - Column( + Scaffold( + snackbarHost = { BuyOrNotSnackBarHost(snackbarHostState) }, + topBar = { BackTopBar(onBackClick = onBackClick) }, + containerColor = BuyOrNotTheme.colors.gray0, + ) { innerPadding -> + Box( modifier = Modifier .fillMaxSize() - .background(BuyOrNotTheme.colors.gray0), + .padding(innerPadding), ) { - BackTopBar(onBackClick = onBackClick) - when { uiState.isLoading -> { Box( @@ -117,7 +143,10 @@ fun NotificationDetailScreen( buyVoteCount = feed.yesCount, maybeVoteCount = feed.noCount, totalVoteCount = feed.totalCount, + isOwner = uiState.isOwner, onVote = { /* 이미 종료된 투표이기 때문에 투표 기능 미구현 */ }, + onDeleteClick = { onIntent(NotificationDetailIntent.OnDeleteClicked) }, + onReportClick = { onIntent(NotificationDetailIntent.OnReportClicked) }, ) } } diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt index b741855..05b6bde 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt @@ -1,11 +1,14 @@ package com.sseotdabwa.buyornot.feature.notification.ui +import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.sseotdabwa.buyornot.core.common.util.runCatchingCancellable +import com.sseotdabwa.buyornot.core.designsystem.icon.BuyOrNotIcons import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel import com.sseotdabwa.buyornot.domain.repository.FeedRepository import com.sseotdabwa.buyornot.domain.repository.NotificationRepository +import com.sseotdabwa.buyornot.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -15,12 +18,15 @@ class NotificationDetailViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val feedRepository: FeedRepository, private val notificationRepository: NotificationRepository, + private val userRepository: UserRepository, ) : BaseViewModel( NotificationDetailUiState(), ) { private val notificationId: Long = checkNotNull(savedStateHandle["notificationId"]) private val feedId: Long = checkNotNull(savedStateHandle["feedId"]) + private var currentUserId: Long? = null + init { loadDetail() markAsRead() @@ -29,6 +35,8 @@ class NotificationDetailViewModel @Inject constructor( override fun handleIntent(intent: NotificationDetailIntent) { when (intent) { NotificationDetailIntent.OnRefresh -> loadDetail() + NotificationDetailIntent.OnDeleteClicked -> handleDelete() + NotificationDetailIntent.OnReportClicked -> handleReport() } } @@ -36,15 +44,71 @@ class NotificationDetailViewModel @Inject constructor( viewModelScope.launch { updateState { it.copy(isLoading = true, isError = false) } runCatchingCancellable { + if (currentUserId == null) { + currentUserId = userRepository.getMyProfile().id + } feedRepository.getFeed(feedId) }.onSuccess { feed -> - updateState { it.copy(isLoading = false, feed = feed) } + val isOwner = currentUserId != null && feed.author.userId == currentUserId + updateState { it.copy(isLoading = false, feed = feed, isOwner = isOwner) } }.onFailure { updateState { it.copy(isLoading = false, isError = true) } } } } + private fun handleDelete() { + viewModelScope.launch { + runCatchingCancellable { + feedRepository.deleteFeed(feedId) + }.onSuccess { + sendSideEffect( + NotificationDetailSideEffect.ShowSnackbar( + message = "삭제가 완료되었습니다.", + icon = BuyOrNotIcons.CheckCircle, + ), + ) + sendSideEffect(NotificationDetailSideEffect.NavigateBack) + }.onFailure { e -> + Log.e("NotificationDetailViewModel", "Failed to delete feed: $feedId", e) + sendSideEffect( + NotificationDetailSideEffect.ShowSnackbar( + message = "삭제에 실패했습니다.", + icon = null, + ), + ) + } + } + } + + private fun handleReport() { + viewModelScope.launch { + runCatchingCancellable { + feedRepository.reportFeed(feedId) + }.onSuccess { + sendSideEffect( + NotificationDetailSideEffect.ShowSnackbar( + message = "신고가 완료되었습니다.", + icon = BuyOrNotIcons.CheckCircle, + ), + ) + }.onFailure { e -> + Log.e("NotificationDetailViewModel", "Failed to report feed: $feedId", e) + val errorMessage = + when { + e.message?.contains("400") == true -> "이미 신고한 피드이거나 본인의 피드입니다." + else -> "신고에 실패했습니다." + } + sendSideEffect( + NotificationDetailSideEffect.ShowSnackbar( + message = errorMessage, + icon = null, + ), + ) + } + } + } + private fun markAsRead() { viewModelScope.launch { runCatchingCancellable { From b5ac89e918768026b70e327098dca076ce795e57 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 24 Feb 2026 23:15:00 +0900 Subject: [PATCH 03/16] =?UTF-8?q?refactor/#60:=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EA=B0=80=EA=B2=A9=20=EC=B2=9C=20=EB=8B=A8=EC=9C=84=20=EC=BD=A4?= =?UTF-8?q?=EB=A7=88=20=ED=8F=AC=EB=A7=B7=ED=8C=85=EC=9D=84=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=B3=80=ED=99=98=20=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buyornot/core/data/repository/FeedRepositoryImpl.kt | 2 +- .../java/com/sseotdabwa/buyornot/domain/model/Feed.kt | 2 +- .../com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt | 2 +- .../sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt | 2 +- .../feature/notification/ui/NotificationDetailScreen.kt | 4 ++-- .../notification/ui/NotificationDetailViewModel.kt | 8 +++++++- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.kt index a2348b1..02532cd 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.kt @@ -150,7 +150,7 @@ private fun FeedItemDto.toDomain(): Feed = Feed( feedId = feedId, content = content, - price = price, + price = String.format(java.util.Locale.KOREA, "%,d", price), category = category.toFeedCategory(), yesCount = yesCount, noCount = noCount, diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt index 0ca5ede..c4bbdbb 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt @@ -6,7 +6,7 @@ package com.sseotdabwa.buyornot.domain.model data class Feed( val feedId: Long, val content: String, - val price: Int, + val price: String, val category: FeedCategory, val yesCount: Int, val noCount: Int, diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt index 1e7d41f..63df189 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt @@ -579,7 +579,7 @@ private fun FeedItemCard( createdAt = feed.createdAt, content = feed.content, productImageUrl = feed.productImageUrl, - price = String.format(java.util.Locale.KOREA, "%,d", feed.price), + price = feed.price, imageAspectRatio = feed.imageAspectRatio, isVoteEnded = feed.isVoteEnded, userVotedOptionIndex = feed.userVotedOptionIndex, diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt index ac648b8..ec74347 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt @@ -442,7 +442,7 @@ class HomeViewModel @Inject constructor( createdAt = TimeUtils.formatRelativeTime(createdAt), content = content, productImageUrl = viewUrl, - price = price.toString(), + price = price, imageAspectRatio = aspectRatio, isVoteEnded = feedStatus == FeedStatus.CLOSED, userVotedOptionIndex = diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt index 91224a5..c571efe 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt @@ -126,7 +126,7 @@ fun NotificationDetailScreen( createdAt = TimeUtils.formatRelativeTime(feed.createdAt), content = feed.content, productImageUrl = feed.viewUrl, - price = String.format(java.util.Locale.KOREA, "%,d", feed.price), + price = feed.price, imageAspectRatio = if (feed.imageWidth > 0 && feed.imageHeight > 0) { if (feed.imageHeight > feed.imageWidth) ImageAspectRatio.PORTRAIT else ImageAspectRatio.SQUARE @@ -167,7 +167,7 @@ private fun NotificationDetailScreenPreview() { Feed( feedId = 1L, content = "이거 어때요? 투표 결과가 궁금해요!", - price = 35000, + price = "35,000", category = FeedCategory.BOOK, yesCount = 80, noCount = 20, diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt index 05b6bde..4b14577 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt @@ -50,7 +50,13 @@ class NotificationDetailViewModel @Inject constructor( feedRepository.getFeed(feedId) }.onSuccess { feed -> val isOwner = currentUserId != null && feed.author.userId == currentUserId - updateState { it.copy(isLoading = false, feed = feed, isOwner = isOwner) } + updateState { + it.copy( + isLoading = false, + feed = feed, + isOwner = isOwner, + ) + } }.onFailure { updateState { it.copy(isLoading = false, isError = true) } } From 2307e88e9d4805373b76c201ca5f29a20765dfa9 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 24 Feb 2026 23:37:58 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat/#60:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=A0=95=EB=B3=B4=20=EB=A1=9C=EC=BB=AC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=ED=88=AC=ED=91=9C=EC=9E=90=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserPreferencesRepositoryImpl.kt | 36 ++++++++++ .../core/datastore/UserPreferences.kt | 1 + .../datastore/UserPreferencesDataSource.kt | 2 + .../UserPreferencesDataSourceImpl.kt | 8 +++ .../core/designsystem/components/FeedCard.kt | 21 +++--- .../repository/UserPreferencesRepository.kt | 22 +++++++ .../feature/auth/ui/LoginViewModel.kt | 15 +++++ .../buyornot/feature/home/ui/HomeContract.kt | 1 + .../buyornot/feature/home/ui/HomeScreen.kt | 3 + .../buyornot/feature/home/ui/HomeViewModel.kt | 66 ++++++++++--------- .../mypage/viewmodel/MyPageViewModel.kt | 4 ++ .../ui/NotificationDetailContract.kt | 1 + .../ui/NotificationDetailScreen.kt | 1 + .../ui/NotificationDetailViewModel.kt | 14 ++++ 14 files changed, 153 insertions(+), 42 deletions(-) diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt index 5794c26..174a70f 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt @@ -1,11 +1,14 @@ package com.sseotdabwa.buyornot.core.data.repository import com.sseotdabwa.buyornot.core.datastore.UserPreferencesDataSource +import com.sseotdabwa.buyornot.domain.model.UserPreferences +import com.sseotdabwa.buyornot.domain.model.UserToken import com.sseotdabwa.buyornot.domain.model.UserType import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject +import com.sseotdabwa.buyornot.core.datastore.UserPreferences as DatastoreUserPreferences import com.sseotdabwa.buyornot.core.datastore.UserType as DatastoreUserType /** @@ -15,14 +18,47 @@ import com.sseotdabwa.buyornot.core.datastore.UserType as DatastoreUserType class UserPreferencesRepositoryImpl @Inject constructor( private val userPreferencesDataSource: UserPreferencesDataSource, ) : UserPreferencesRepository { + override val userPreferences: Flow = + userPreferencesDataSource.preferences.map { it.toDomain() } + + override val userToken: Flow = + userPreferencesDataSource.preferences.map { it.toTokenDomain() } + override val userType: Flow = userPreferencesDataSource.userType.map { it.toDomain() } override suspend fun updateUserType(userType: UserType) { userPreferencesDataSource.updateUserType(userType.toDatastore()) } + + override suspend fun updateDisplayName(newName: String) { + userPreferencesDataSource.updateDisplayName(newName) + } + + override suspend fun updateProfileImageUrl(newUrl: String) { + userPreferencesDataSource.updateProfileImageUrl(newUrl) + } } +/** + * DataStore UserPreferences → Domain UserPreferences + */ +private fun DatastoreUserPreferences.toDomain(): UserPreferences = + UserPreferences( + displayName = displayName, + profileImageUrl = profileImageUrl, + userType = userType.toDomain(), + ) + +/** + * DataStore UserPreferences → Domain UserToken + */ +private fun DatastoreUserPreferences.toTokenDomain(): UserToken = + UserToken( + accessToken = accessToken, + refreshToken = refreshToken, + ) + /** * DataStore UserType → Domain UserType */ diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt index e8ac41e..b9ed508 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt @@ -15,6 +15,7 @@ enum class UserType { */ data class UserPreferences( val displayName: String = "손님", + val profileImageUrl: String = "", val accessToken: String = "", val refreshToken: String = "", val userType: UserType = UserType.GUEST, diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt index c0ae9d5..a74e9dc 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt @@ -16,6 +16,8 @@ interface UserPreferencesDataSource { suspend fun updateDisplayName(newName: String) + suspend fun updateProfileImageUrl(newUrl: String) + suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt index cd7afcd..01cfe1a 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt @@ -25,6 +25,7 @@ class UserPreferencesDataSourceImpl ) : UserPreferencesDataSource { private object Keys { val DISPLAY_NAME = stringPreferencesKey("display_name") + val PROFILE_IMAGE_URL = stringPreferencesKey("profile_image_url") val ACCESS_TOKEN = stringPreferencesKey("access_token") val REFRESH_TOKEN = stringPreferencesKey("refresh_token") val USER_TYPE = stringPreferencesKey("user_type") @@ -34,6 +35,7 @@ class UserPreferencesDataSourceImpl context.userPreferencesDataStore.data.map { prefs -> UserPreferences( displayName = prefs[Keys.DISPLAY_NAME] ?: UserPreferences().displayName, + profileImageUrl = prefs[Keys.PROFILE_IMAGE_URL] ?: UserPreferences().profileImageUrl, accessToken = prefs[Keys.ACCESS_TOKEN] ?: UserPreferences().accessToken, refreshToken = prefs[Keys.REFRESH_TOKEN] ?: UserPreferences().refreshToken, userType = @@ -66,6 +68,12 @@ class UserPreferencesDataSourceImpl } } + override suspend fun updateProfileImageUrl(newUrl: String) { + context.userPreferencesDataStore.edit { prefs -> + prefs[Keys.PROFILE_IMAGE_URL] = newUrl + } + } + override suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt b/core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt index 6a2ee85..40c77e3 100644 --- a/core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt +++ b/core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt @@ -72,6 +72,7 @@ fun FeedCard( totalVoteCount: Int, onVote: (Int) -> Unit, // 투표 옵션 인덱스 (0: 사!, 1: 애매..) isOwner: Boolean = false, // 본인 글인지 여부 + voterProfileImageUrl: String = "", // 사용자가 투표한 경우의 프로필 이미지 URL onDeleteClick: () -> Unit = {}, // 삭제 클릭 콜백 추가 onReportClick: () -> Unit = {}, // 신고 클릭 콜백 추가 ) { @@ -265,15 +266,15 @@ fun FeedCard( leadingContent = if (userVotedOptionIndex == 0) { { - Box( + AsyncImage( + model = voterProfileImageUrl, + contentDescription = null, modifier = Modifier .height(20.dp) .width(20.dp) - .background( - color = BuyOrNotTheme.colors.gray500, - shape = RoundedCornerShape(10.dp), - ), + .clip(CircleShape), + contentScale = ContentScale.Crop, ) } } else { @@ -289,15 +290,15 @@ fun FeedCard( leadingContent = if (userVotedOptionIndex == 1) { { - Box( + AsyncImage( + model = voterProfileImageUrl, + contentDescription = null, modifier = Modifier .height(20.dp) .width(20.dp) - .background( - color = BuyOrNotTheme.colors.gray500, - shape = RoundedCornerShape(10.dp), - ), + .clip(CircleShape), + contentScale = ContentScale.Crop, ) } } else { diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt index f993cfe..aea959d 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt @@ -1,5 +1,7 @@ package com.sseotdabwa.buyornot.domain.repository +import com.sseotdabwa.buyornot.domain.model.UserPreferences +import com.sseotdabwa.buyornot.domain.model.UserToken import com.sseotdabwa.buyornot.domain.model.UserType import kotlinx.coroutines.flow.Flow @@ -7,6 +9,16 @@ import kotlinx.coroutines.flow.Flow * 사용자 정보 Repository */ interface UserPreferencesRepository { + /** + * 전체 사용자 정보를 Flow로 제공 + */ + val userPreferences: Flow + + /** + * 인증 토큰 정보를 Flow로 제공 + */ + val userToken: Flow + /** * 현재 사용자 타입을 Flow로 제공 */ @@ -16,4 +28,14 @@ interface UserPreferencesRepository { * 사용자 타입 업데이트 */ suspend fun updateUserType(userType: UserType) + + /** + * 표시 이름 업데이트 + */ + suspend fun updateDisplayName(newName: String) + + /** + * 프로필 이미지 URL 업데이트 + */ + suspend fun updateProfileImageUrl(newUrl: String) } diff --git a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt index 8ecdee8..5709026 100644 --- a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt @@ -101,6 +101,7 @@ class LoginViewModel @Inject constructor( runCatchingCancellable { authRepository.googleLogin(idToken) }.onSuccess { + fetchAndStoreUserProfile() updateFcmToken() sendSideEffect(LoginSideEffect.NavigateToHome) }.onFailure { @@ -157,6 +158,7 @@ class LoginViewModel @Inject constructor( runCatchingCancellable { authRepository.kakaoLogin(accessToken) }.onSuccess { + fetchAndStoreUserProfile() updateFcmToken() sendSideEffect(LoginSideEffect.NavigateToHome) }.onFailure { @@ -166,6 +168,19 @@ class LoginViewModel @Inject constructor( } } + private fun fetchAndStoreUserProfile() { + viewModelScope.launch { + runCatchingCancellable { + userRepository.getMyProfile() + }.onSuccess { profile -> + userPreferencesRepository.updateDisplayName(profile.nickname) + userPreferencesRepository.updateProfileImageUrl(profile.profileImage) + }.onFailure { e -> + Log.e(TAG, "Failed to fetch user profile after login", e) + } + } + } + /** * FCM 토큰을 가져와 서버에 업데이트합니다. */ diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.kt index 0ffac45..a974a37 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.kt @@ -63,6 +63,7 @@ data class HomeUiState( val selectedTab: HomeTab = HomeTab.FEED, val selectedFilter: FilterChip = FilterChip.ALL, val isBannerVisible: Boolean = true, + val voterProfileImageUrl: String = "", val feeds: List = emptyList(), val isLoading: Boolean = true, val isNextPageLoading: Boolean = false, diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt index 63df189..acc995c 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt @@ -476,6 +476,7 @@ private fun HomeFeedList( items(filteredFeeds.size, key = { index -> filteredFeeds[index].id }) { index -> FeedItemCard( feed = filteredFeeds[index], + voterProfileImageUrl = uiState.voterProfileImageUrl, modifier = Modifier.padding(20.dp).animateItem(), onVote = { id, opt -> onIntent(HomeIntent.OnVoteClicked(id, opt)) }, onDelete = { id -> onIntent(HomeIntent.OnDeleteClicked(id)) }, @@ -565,6 +566,7 @@ private fun FilterChipRow( @Composable private fun FeedItemCard( feed: FeedItem, + voterProfileImageUrl: String, modifier: Modifier = Modifier, onVote: (String, Int) -> Unit, onDelete: (String) -> Unit, @@ -587,6 +589,7 @@ private fun FeedItemCard( maybeVoteCount = feed.maybeVoteCount, totalVoteCount = feed.totalVoteCount, isOwner = feed.isOwner, + voterProfileImageUrl = voterProfileImageUrl, onVote = { option -> onVote(feed.id, option) }, diff --git a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt index ec74347..e892e96 100644 --- a/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt +++ b/feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt @@ -15,8 +15,6 @@ import com.sseotdabwa.buyornot.domain.repository.FeedRepository import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository import com.sseotdabwa.buyornot.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -35,13 +33,41 @@ class HomeViewModel @Inject constructor( private var isUserIdLoaded = false // ID 로드 완료 여부 추적 init { - observeUserType() + observeUserPreferences() loadInitialData() } + private fun observeUserPreferences() { + viewModelScope.launch { + var lastUserType: UserType? = null + userPreferencesRepository.userPreferences + .collect { preferences -> + val userType = preferences.userType + updateState { + it.copy( + userType = userType, + voterProfileImageUrl = preferences.profileImageUrl, + ) + } + + if (lastUserType != userType) { + if (userType == UserType.SOCIAL) { + loadUserIdAndRefreshFeeds() + } else { + currentUserId = null + isUserIdLoaded = true + // 게스트 전환 시 탭을 무조건 FEED로 변경 + updateState { it.copy(selectedTab = HomeTab.FEED) } + loadFeeds(HomeTab.FEED) + } + lastUserType = userType + } + } + } + } + /** * 초기 데이터 로드: 사용자 ID를 먼저 로드한 후 피드 로드 - * 경쟁 조건을 방지하기 위해 순차적으로 실행 */ private fun loadInitialData() { viewModelScope.launch { @@ -59,7 +85,10 @@ class HomeViewModel @Inject constructor( private suspend fun loadCurrentUserIdSuspend() { runCatchingCancellable { if (uiState.value.userType == UserType.SOCIAL) { - userRepository.getMyProfile().id + val profile = userRepository.getMyProfile() + userPreferencesRepository.updateDisplayName(profile.nickname) + userPreferencesRepository.updateProfileImageUrl(profile.profileImage) + profile.id } else { null } @@ -71,33 +100,6 @@ class HomeViewModel @Inject constructor( } } - private fun observeUserType() { - viewModelScope.launch { - userPreferencesRepository.userType - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = UserType.GUEST, - ).collect { userType -> - updateState { it.copy(userType = userType) } - // 사용자 타입이 변경되면 userId를 다시 로드하고 피드도 갱신 - if (userType == UserType.SOCIAL) { - loadUserIdAndRefreshFeeds() - } else { - currentUserId = null - isUserIdLoaded = true - // 게스트 전환 시 탭을 무조건 FEED로 변경 - updateState { it.copy(selectedTab = HomeTab.FEED) } - loadFeeds(HomeTab.FEED) - } - } - } - } - - /** - * 사용자 ID를 로드하고 피드를 갱신 - * 로그인 후 자신의 피드에 대한 isOwner를 올바르게 설정 - */ private fun loadUserIdAndRefreshFeeds() { viewModelScope.launch { loadCurrentUserIdSuspend() diff --git a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt index cc391b5..a1fe093 100644 --- a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt @@ -3,6 +3,7 @@ package com.sseotdabwa.buyornot.feature.mypage.viewmodel import androidx.lifecycle.viewModelScope import com.sseotdabwa.buyornot.core.common.util.runCatchingCancellable import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel +import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository import com.sseotdabwa.buyornot.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -11,6 +12,7 @@ import javax.inject.Inject @HiltViewModel class MyPageViewModel @Inject constructor( private val userRepository: UserRepository, + private val userPreferencesRepository: UserPreferencesRepository, ) : BaseViewModel(MyPageUiState()) { init { handleIntent(MyPageIntent.LoadProfile) @@ -29,6 +31,8 @@ class MyPageViewModel @Inject constructor( userRepository.getMyProfile() }.onSuccess { profile -> updateState { it.copy(isLoading = false, userProfile = profile) } + userPreferencesRepository.updateDisplayName(profile.nickname) + userPreferencesRepository.updateProfileImageUrl(profile.profileImage) }.onFailure { throwable -> updateState { it.copy(isLoading = false) } sendSideEffect(MyPageSideEffect.ShowSnackbar(throwable.message ?: "프로필을 불러오지 못했습니다.")) diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt index 21bc126..e9bc772 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt @@ -9,6 +9,7 @@ data class NotificationDetailUiState( val isLoading: Boolean = true, val isError: Boolean = false, val feed: Feed? = null, + val voterProfileImageUrl: String = "", val isOwner: Boolean = false, ) diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt index c571efe..00238e0 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt @@ -144,6 +144,7 @@ fun NotificationDetailScreen( maybeVoteCount = feed.noCount, totalVoteCount = feed.totalCount, isOwner = uiState.isOwner, + voterProfileImageUrl = uiState.voterProfileImageUrl, onVote = { /* 이미 종료된 투표이기 때문에 투표 기능 미구현 */ }, onDeleteClick = { onIntent(NotificationDetailIntent.OnDeleteClicked) }, onReportClick = { onIntent(NotificationDetailIntent.OnReportClicked) }, diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt index 4b14577..e3f0b45 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt @@ -8,6 +8,7 @@ import com.sseotdabwa.buyornot.core.designsystem.icon.BuyOrNotIcons import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel import com.sseotdabwa.buyornot.domain.repository.FeedRepository import com.sseotdabwa.buyornot.domain.repository.NotificationRepository +import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository import com.sseotdabwa.buyornot.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -19,6 +20,7 @@ class NotificationDetailViewModel @Inject constructor( private val feedRepository: FeedRepository, private val notificationRepository: NotificationRepository, private val userRepository: UserRepository, + private val userPreferencesRepository: UserPreferencesRepository, ) : BaseViewModel( NotificationDetailUiState(), ) { @@ -28,10 +30,22 @@ class NotificationDetailViewModel @Inject constructor( private var currentUserId: Long? = null init { + observeUserPreferences() loadDetail() markAsRead() } + private fun observeUserPreferences() { + viewModelScope.launch { + userPreferencesRepository.userPreferences + .collect { preferences -> + updateState { + it.copy(voterProfileImageUrl = preferences.profileImageUrl) + } + } + } + } + override fun handleIntent(intent: NotificationDetailIntent) { when (intent) { NotificationDetailIntent.OnRefresh -> loadDetail() From f9029131fc4c1d2dc3d580a53766924890af7429 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 24 Feb 2026 23:50:12 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix/#60:=20=EC=95=B1=20=EC=B5=9C=EC=B4=88?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=EC=97=90=EB=A7=8C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B6=8C=ED=95=9C=EC=9D=84=20=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt | 17 +++++++--- .../buyornot/ui/BuyOrNotViewModel.kt | 31 +++++++++++++++++++ .../UserPreferencesRepositoryImpl.kt | 5 +++ .../core/datastore/UserPreferences.kt | 1 + .../datastore/UserPreferencesDataSource.kt | 2 ++ .../UserPreferencesDataSourceImpl.kt | 9 ++++++ .../buyornot/domain/model/UserPreferences.kt | 11 +++++++ .../buyornot/domain/model/UserToken.kt | 9 ++++++ .../repository/UserPreferencesRepository.kt | 5 +++ 9 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt create mode 100644 domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt create mode 100644 domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.kt diff --git a/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt index 832f555..778acf3 100644 --- a/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt +++ b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController @@ -37,28 +39,35 @@ import com.sseotdabwa.buyornot.navigation.BuyOrNotNavHost * * @param authEventBus 인증 관련 이벤트 버스 * @param onBackPressed 홈 화면에서 뒤로가기 시 앱 종료를 위한 콜백 + * @param viewModel 앱 공통 ViewModel */ @Composable fun BuyOrNotApp( authEventBus: AuthEventBus, onBackPressed: () -> Unit = {}, + viewModel: BuyOrNotViewModel = hiltViewModel(), ) { val navController = rememberNavController() val snackbarState = rememberBuyOrNotSnackbarState() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination + val isFirstRun by viewModel.isFirstRun.collectAsStateWithLifecycle() + // 홈 화면에서 뒤로가기 시 앱 종료 BackHandler(enabled = currentDestination?.route == HOME_ROUTE) { onBackPressed() } - // 앱 진입 시 알림 권한 자동 요청 + // 앱 진입 시 최초 1회만 알림 권한 자동 요청 val (hasNotificationPermission, requestNotificationPermission) = rememberNotificationPermission() - LaunchedEffect(Unit) { - if (!hasNotificationPermission) { - requestNotificationPermission() + LaunchedEffect(isFirstRun) { + if (isFirstRun) { + if (!hasNotificationPermission) { + requestNotificationPermission() + } + viewModel.updateIsFirstRun(false) } } diff --git a/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt new file mode 100644 index 0000000..7983830 --- /dev/null +++ b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt @@ -0,0 +1,31 @@ +package com.sseotdabwa.buyornot.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class BuyOrNotViewModel @Inject constructor( + private val userPreferencesRepository: UserPreferencesRepository, +) : ViewModel() { + val isFirstRun = + userPreferencesRepository.userPreferences + .map { it.isFirstRun } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false, + ) + + fun updateIsFirstRun(isFirstRun: Boolean) { + viewModelScope.launch { + userPreferencesRepository.updateIsFirstRun(isFirstRun) + } + } +} diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt index 174a70f..afaaa98 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt @@ -38,6 +38,10 @@ class UserPreferencesRepositoryImpl @Inject constructor( override suspend fun updateProfileImageUrl(newUrl: String) { userPreferencesDataSource.updateProfileImageUrl(newUrl) } + + override suspend fun updateIsFirstRun(isFirstRun: Boolean) { + userPreferencesDataSource.updateIsFirstRun(isFirstRun) + } } /** @@ -48,6 +52,7 @@ private fun DatastoreUserPreferences.toDomain(): UserPreferences = displayName = displayName, profileImageUrl = profileImageUrl, userType = userType.toDomain(), + isFirstRun = isFirstRun, ) /** diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt index b9ed508..7c901ba 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt @@ -19,4 +19,5 @@ data class UserPreferences( val accessToken: String = "", val refreshToken: String = "", val userType: UserType = UserType.GUEST, + val isFirstRun: Boolean = true, ) diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt index a74e9dc..50c40fa 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt @@ -18,6 +18,8 @@ interface UserPreferencesDataSource { suspend fun updateProfileImageUrl(newUrl: String) + suspend fun updateIsFirstRun(isFirstRun: Boolean) + suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt index 01cfe1a..40f0aba 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.sseotdabwa.buyornot.core.datastore import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -29,6 +30,7 @@ class UserPreferencesDataSourceImpl val ACCESS_TOKEN = stringPreferencesKey("access_token") val REFRESH_TOKEN = stringPreferencesKey("refresh_token") val USER_TYPE = stringPreferencesKey("user_type") + val IS_FIRST_RUN = booleanPreferencesKey("is_first_run") } override val preferences: Flow = @@ -46,6 +48,7 @@ class UserPreferencesDataSourceImpl UserPreferences().userType } } ?: UserPreferences().userType, + isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, ) } @@ -74,6 +77,12 @@ class UserPreferencesDataSourceImpl } } + override suspend fun updateIsFirstRun(isFirstRun: Boolean) { + context.userPreferencesDataStore.edit { prefs -> + prefs[Keys.IS_FIRST_RUN] = isFirstRun + } + } + override suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt new file mode 100644 index 0000000..dfe991f --- /dev/null +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt @@ -0,0 +1,11 @@ +package com.sseotdabwa.buyornot.domain.model + +/** + * 사용자 정보 도메인 모델 + */ +data class UserPreferences( + val displayName: String, + val profileImageUrl: String, + val userType: UserType, + val isFirstRun: Boolean, +) diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.kt new file mode 100644 index 0000000..7662153 --- /dev/null +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.kt @@ -0,0 +1,9 @@ +package com.sseotdabwa.buyornot.domain.model + +/** + * 인증 토큰 정보 도메인 모델 + */ +data class UserToken( + val accessToken: String, + val refreshToken: String, +) diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt index aea959d..4436dc4 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt @@ -38,4 +38,9 @@ interface UserPreferencesRepository { * 프로필 이미지 URL 업데이트 */ suspend fun updateProfileImageUrl(newUrl: String) + + /** + * 최초 실행 여부 업데이트 + */ + suspend fun updateIsFirstRun(isFirstRun: Boolean) } From 15018c2918ef3d1a47b38640970f4b2deb9afffc Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 00:30:55 +0900 Subject: [PATCH 06/16] =?UTF-8?q?fix/#60:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A0=80=EC=9E=A5=20suspend=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buyornot/feature/auth/ui/LoginViewModel.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt index 5709026..1687283 100644 --- a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt @@ -168,16 +168,14 @@ class LoginViewModel @Inject constructor( } } - private fun fetchAndStoreUserProfile() { - viewModelScope.launch { - runCatchingCancellable { - userRepository.getMyProfile() - }.onSuccess { profile -> - userPreferencesRepository.updateDisplayName(profile.nickname) - userPreferencesRepository.updateProfileImageUrl(profile.profileImage) - }.onFailure { e -> - Log.e(TAG, "Failed to fetch user profile after login", e) - } + private suspend fun fetchAndStoreUserProfile() { + runCatchingCancellable { + userRepository.getMyProfile() + }.onSuccess { profile -> + userPreferencesRepository.updateDisplayName(profile.nickname) + userPreferencesRepository.updateProfileImageUrl(profile.profileImage) + }.onFailure { e -> + Log.e(TAG, "Failed to fetch user profile after login", e) } } From 89a9f6b23e389629313f0fbd50774dd69397a22d Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 00:35:04 +0900 Subject: [PATCH 07/16] =?UTF-8?q?fix/#60:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EB=A1=9C=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/mypage/viewmodel/MyPageViewModel.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt index a1fe093..4e001d9 100644 --- a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt @@ -1,5 +1,6 @@ package com.sseotdabwa.buyornot.feature.mypage.viewmodel +import android.util.Log import androidx.lifecycle.viewModelScope import com.sseotdabwa.buyornot.core.common.util.runCatchingCancellable import com.sseotdabwa.buyornot.core.ui.base.BaseViewModel @@ -9,6 +10,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +private const val TAG = "MyPageViewModel" + @HiltViewModel class MyPageViewModel @Inject constructor( private val userRepository: UserRepository, @@ -31,11 +34,16 @@ class MyPageViewModel @Inject constructor( userRepository.getMyProfile() }.onSuccess { profile -> updateState { it.copy(isLoading = false, userProfile = profile) } - userPreferencesRepository.updateDisplayName(profile.nickname) - userPreferencesRepository.updateProfileImageUrl(profile.profileImage) + runCatchingCancellable { + userPreferencesRepository.updateDisplayName(profile.nickname) + userPreferencesRepository.updateProfileImageUrl(profile.profileImage) + }.onFailure { + Log.w(TAG, "Failed to update user preferences") + } }.onFailure { throwable -> updateState { it.copy(isLoading = false) } - sendSideEffect(MyPageSideEffect.ShowSnackbar(throwable.message ?: "프로필을 불러오지 못했습니다.")) + sendSideEffect(MyPageSideEffect.ShowSnackbar("프로필을 불러오지 못했습니다.")) + Log.w(TAG, throwable.toString()) } } } From 29d3b299c7d0640a5df7cc833154572fe7c1d4b8 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 00:37:09 +0900 Subject: [PATCH 08/16] =?UTF-8?q?fix/#60:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=A0=84=ED=8C=8C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/ui/NotificationDetailViewModel.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt index e3f0b45..0d8f754 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt @@ -14,6 +14,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +private const val TAG = "NotificationDetailViewModel" + @HiltViewModel class NotificationDetailViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, @@ -59,7 +61,13 @@ class NotificationDetailViewModel @Inject constructor( updateState { it.copy(isLoading = true, isError = false) } runCatchingCancellable { if (currentUserId == null) { - currentUserId = userRepository.getMyProfile().id + runCatchingCancellable { + userRepository.getMyProfile().id + }.onSuccess { id -> + currentUserId = id + }.onFailure { + Log.w(TAG, "Failed to get current user ID") + } } feedRepository.getFeed(feedId) }.onSuccess { feed -> From 4e7cab9fef987b2030128cd3e94e1ee29dc374f8 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 00:43:04 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor/#60:=20NotificationDetailViewMod?= =?UTF-8?q?el=EC=9D=98=20=EB=A1=9C=EA=B7=B8=20=ED=83=9C=EA=B7=B8=EB=A5=BC?= =?UTF-8?q?=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/notification/ui/NotificationDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt index 0d8f754..b70ee18 100644 --- a/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt +++ b/feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt @@ -98,7 +98,7 @@ class NotificationDetailViewModel @Inject constructor( ) sendSideEffect(NotificationDetailSideEffect.NavigateBack) }.onFailure { e -> - Log.e("NotificationDetailViewModel", "Failed to delete feed: $feedId", e) + Log.e(TAG, "Failed to delete feed: $feedId", e) sendSideEffect( NotificationDetailSideEffect.ShowSnackbar( message = "삭제에 실패했습니다.", From 2ac8fb4c86aa1cd594ad5b3532419de23746257c Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 00:51:30 +0900 Subject: [PATCH 10/16] =?UTF-8?q?chore/#60:=20Proguard=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/proguard-rules.pro | 139 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 14 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index b44e833..a34574f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,60 +1,171 @@ +# ============================================================ # [Common] Android +# ============================================================ -keepattributes SourceFile,LineNumberTable -keepattributes *Annotation* -keepattributes Signature -keepattributes InnerClasses +-keepattributes Exceptions -dontwarn sun.misc.** -dontwarn javax.annotation.** +# Enum 보존 +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Parcelable 보존 +-keep class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# Serializable 보존 +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); +} + +# ============================================================ # [Kotlin] +# ============================================================ -keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { + ; +} + +# ============================================================ +# [Kotlinx Coroutines] +# ============================================================ +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { + volatile ; +} +-dontwarn kotlinx.coroutines.** + +# ============================================================ +# [Kotlinx Serialization] +# ============================================================ +-keep class kotlinx.serialization.** { *; } +-keepclassmembers @kotlinx.serialization.Serializable class * { + *** Companion; + *** INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} +-keepclassmembers class * { + @kotlinx.serialization.Serializable *; +} +-keepclassmembers class * { + kotlinx.serialization.KSerializer serializer(...); +} +-keepclassmembers class kotlinx.serialization.json.** { + *** Companion; +} +-dontwarn kotlinx.serialization.** +# ============================================================ # [Hilt / Dagger] +# ============================================================ -keep class dagger.hilt.** { *; } -keep class com.google.dagger.** { *; } --keep @dagger.hilt.android.lifecycle.HiltViewModel class * +-keep @dagger.hilt.android.lifecycle.HiltViewModel class * { *; } +-keep @dagger.hilt.InstallIn class * { *; } -keep interface * extends javax.inject.Provider +-dontwarn dagger.hilt.** +# ============================================================ # [Retrofit / OkHttp] +# ============================================================ -keep class retrofit2.** { *; } -dontwarn retrofit2.** --keepattributes *Annotation* -keepclassmembers class * { @retrofit2.http.* ; } +-keepclassmembernames interface * { + @retrofit2.http.* ; +} -keep class okhttp3.** { *; } -dontwarn okhttp3.** -dontwarn okio.** -# [Kotlinx Serialization] --keepattributes *Annotation*, InnerClasses +# ============================================================ +# [Jetpack Compose] +# ============================================================ +-keep class androidx.compose.runtime.RecomposeScopeImpl { *; } +-keep class * implements androidx.compose.runtime.Stable { *; } -keepclassmembers class * { - @kotlinx.serialization.Serializable *; + @androidx.compose.runtime.Composable ; } --keepclassmembers class * { - kotlinx.serialization.KSerializer serializer(...); + +# ============================================================ +# [AndroidX Lifecycle / ViewModel] +# ============================================================ +-keep class * extends androidx.lifecycle.ViewModel { *; } +-keep class * extends androidx.lifecycle.AndroidViewModel { *; } +-keepclassmembers class * extends androidx.lifecycle.ViewModel { + (...); } -# [Domain/Data Models] - API 통신 데이터 클래스 보존 -# 모든 모듈의 domain.model, network.model 하위의 클래스들을 보존합니다. --keep class com.sseotdabwa.buyornot.domain.model.** { *; } --keep class com.sseotdabwa.buyornot.core.network.model.** { *; } +# ============================================================ +# [Navigation Compose] +# ============================================================ +-keep class androidx.navigation.** { *; } + +# ============================================================ +# [AndroidX DataStore] +# ============================================================ +-keep class androidx.datastore.** { *; } + +# ============================================================ +# [AndroidX Credentials / Google Sign-In] +# ============================================================ +-keep class androidx.credentials.** { *; } +-keep class com.google.android.libraries.identity.googleid.** { *; } +-dontwarn com.google.android.libraries.identity.googleid.** +# ============================================================ # [Coil] +# ============================================================ -keep class coil.** { *; } -dontwarn coil.** +# ============================================================ # [Firebase] +# ============================================================ -keep class com.google.firebase.** { *; } -dontwarn com.google.firebase.** +# Crashlytics: 스택 트레이스 보존을 위해 Exception 하위 클래스 유지 +-keep public class * extends java.lang.Exception + +# ============================================================ # [Lottie] +# ============================================================ -keep class com.airbnb.lottie.** { *; } +-dontwarn com.airbnb.lottie.** +# ============================================================ # [Kakao SDK] +# ============================================================ -keep class com.kakao.sdk.** { *; } -dontwarn com.kakao.sdk.** -# [Jetpack Compose] --keep class androidx.compose.runtime.RecomposeScopeImpl { *; } --keep class * implements androidx.compose.runtime.Parcelable { *; } +# ============================================================ +# [Domain / Data Models] - API 통신 데이터 클래스 보존 +# ============================================================ +# Domain models +-keep class com.sseotdabwa.buyornot.domain.model.** { *; } + +# Network DTOs (request / response) +-keep class com.sseotdabwa.buyornot.core.network.dto.** { *; } + +# DataStore preferences holder +-keep class com.sseotdabwa.buyornot.core.datastore.** { *; } + +# ============================================================ +# [App] Firebase Cloud Messaging Service +# ============================================================ +-keep class com.sseotdabwa.buyornot.notification.** { *; } From dc2ecde56a123d00bf2e87bd6fcae17e79e54700 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 22:38:29 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor/#60:=20updateFcmToken=EB=A5=BC?= =?UTF-8?q?=20=EC=A0=95=EC=A7=80=20=ED=95=A8=EC=88=98=EB=A1=9C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EC=96=B4=20=EC=8B=A4=ED=96=89=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/auth/ui/LoginViewModel.kt | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt index 1687283..bc800fb 100644 --- a/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt @@ -23,6 +23,7 @@ import com.sseotdabwa.buyornot.domain.repository.UserRepository import com.sseotdabwa.buyornot.feature.auth.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import java.security.SecureRandom import java.util.Base64 import javax.inject.Inject @@ -182,23 +183,12 @@ class LoginViewModel @Inject constructor( /** * FCM 토큰을 가져와 서버에 업데이트합니다. */ - private fun updateFcmToken() { - FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> - if (!task.isSuccessful) { - Log.w("FCM", "Fetching FCM registration token failed", task.exception) - return@addOnCompleteListener - } - - val token = task.result ?: return@addOnCompleteListener - viewModelScope.launch { - runCatchingCancellable { - userRepository.updateFcmToken(token) - }.onSuccess { - Log.d("FCM", "FCM Token successfully updated to server.") - }.onFailure { e -> - Log.e("FCM", "Failed to update FCM token to server", e) - } - } + private suspend fun updateFcmToken() { + runCatchingCancellable { + val token = FirebaseMessaging.getInstance().token.await() + userRepository.updateFcmToken(token) + }.onFailure { + Log.e("FCM", "Failed to update FCM token to server", it) } } From 5ff947260d42eb5c8fa79dd726c40044fc5c036b Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 22:49:12 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor/#60:=20=EC=B2=AB=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=97=AC=EB=B6=80=EB=A5=BC=20AppPreferences?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt | 10 ++++------ .../data/repository/AppPreferencesRepositoryImpl.kt | 7 +++++++ .../repository/UserPreferencesRepositoryImpl.kt | 5 ----- .../buyornot/core/datastore/AppPreferences.kt | 8 ++------ .../core/datastore/AppPreferencesDataSource.kt | 10 ++++++++++ .../core/datastore/AppPreferencesDataSourceImpl.kt | 13 +++++++++++++ .../buyornot/core/datastore/UserPreferences.kt | 1 - .../core/datastore/UserPreferencesDataSource.kt | 2 -- .../core/datastore/UserPreferencesDataSourceImpl.kt | 9 --------- .../buyornot/domain/model/UserPreferences.kt | 1 - .../domain/repository/AppPreferencesRepository.kt | 10 ++++++++++ .../domain/repository/UserPreferencesRepository.kt | 5 ----- 12 files changed, 46 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt index 7983830..3035439 100644 --- a/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt +++ b/app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt @@ -2,21 +2,19 @@ package com.sseotdabwa.buyornot.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sseotdabwa.buyornot.domain.repository.UserPreferencesRepository +import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class BuyOrNotViewModel @Inject constructor( - private val userPreferencesRepository: UserPreferencesRepository, + private val appPreferencesRepository: AppPreferencesRepository, ) : ViewModel() { val isFirstRun = - userPreferencesRepository.userPreferences - .map { it.isFirstRun } + appPreferencesRepository.isFirstRun .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), @@ -25,7 +23,7 @@ class BuyOrNotViewModel @Inject constructor( fun updateIsFirstRun(isFirstRun: Boolean) { viewModelScope.launch { - userPreferencesRepository.updateIsFirstRun(isFirstRun) + appPreferencesRepository.updateIsFirstRun(isFirstRun) } } } diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.kt index 8dabecb..3f2de7b 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.kt @@ -15,7 +15,14 @@ class AppPreferencesRepositoryImpl @Inject constructor( override val hasRequestedNotificationPermission: Flow = appPreferencesDataSource.hasRequestedNotificationPermission + override val isFirstRun: Flow = + appPreferencesDataSource.isFirstRun + override suspend fun updateNotificationPermissionRequested(requested: Boolean) { appPreferencesDataSource.updateNotificationPermissionRequested(requested) } + + override suspend fun updateIsFirstRun(isFirstRun: Boolean) { + appPreferencesDataSource.updateIsFirstRun(isFirstRun) + } } diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt index afaaa98..174a70f 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt @@ -38,10 +38,6 @@ class UserPreferencesRepositoryImpl @Inject constructor( override suspend fun updateProfileImageUrl(newUrl: String) { userPreferencesDataSource.updateProfileImageUrl(newUrl) } - - override suspend fun updateIsFirstRun(isFirstRun: Boolean) { - userPreferencesDataSource.updateIsFirstRun(isFirstRun) - } } /** @@ -52,7 +48,6 @@ private fun DatastoreUserPreferences.toDomain(): UserPreferences = displayName = displayName, profileImageUrl = profileImageUrl, userType = userType.toDomain(), - isFirstRun = isFirstRun, ) /** diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.kt index e15687a..086097b 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.kt @@ -12,11 +12,7 @@ data class AppPreferences( */ val hasRequestedNotificationPermission: Boolean = false, /** - * 향후 추가될 앱 설정들 - * 예: 다크모드, 언어 설정, 첫 실행 여부 등 - * - * val isDarkModeEnabled: Boolean = false, - * val preferredLanguage: String = "ko", - * val isFirstLaunch: Boolean = true, + * 최초 실행 여부 */ + val isFirstRun: Boolean = true, ) diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.kt index 6e69a29..71d72c1 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.kt @@ -18,8 +18,18 @@ interface AppPreferencesDataSource { */ val hasRequestedNotificationPermission: Flow + /** + * 최초 실행 여부를 Flow로 제공 + */ + val isFirstRun: Flow + /** * 알림 권한 요청 이력 업데이트 */ suspend fun updateNotificationPermissionRequested(requested: Boolean) + + /** + * 최초 실행 여부 업데이트 + */ + suspend fun updateIsFirstRun(isFirstRun: Boolean) } diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt index 32e0736..860bc0d 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt @@ -25,12 +25,14 @@ class AppPreferencesDataSourceImpl ) : AppPreferencesDataSource { private object Keys { val HAS_REQUESTED_NOTIFICATION_PERMISSION = booleanPreferencesKey("has_requested_notification_permission") + val IS_FIRST_RUN = booleanPreferencesKey("is_first_run") } override val preferences: Flow = context.appPreferencesDataStore.data.map { prefs -> AppPreferences( hasRequestedNotificationPermission = prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false, + isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, ) } @@ -39,9 +41,20 @@ class AppPreferencesDataSourceImpl prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false } + override val isFirstRun: Flow = + context.appPreferencesDataStore.data.map { prefs -> + prefs[Keys.IS_FIRST_RUN] ?: true + } + override suspend fun updateNotificationPermissionRequested(requested: Boolean) { context.appPreferencesDataStore.edit { prefs -> prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] = requested } } + + override suspend fun updateIsFirstRun(isFirstRun: Boolean) { + context.appPreferencesDataStore.edit { prefs -> + prefs[Keys.IS_FIRST_RUN] = isFirstRun + } + } } diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt index 7c901ba..b9ed508 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt @@ -19,5 +19,4 @@ data class UserPreferences( val accessToken: String = "", val refreshToken: String = "", val userType: UserType = UserType.GUEST, - val isFirstRun: Boolean = true, ) diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt index 50c40fa..a74e9dc 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt @@ -18,8 +18,6 @@ interface UserPreferencesDataSource { suspend fun updateProfileImageUrl(newUrl: String) - suspend fun updateIsFirstRun(isFirstRun: Boolean) - suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt index 40f0aba..01cfe1a 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt @@ -1,7 +1,6 @@ package com.sseotdabwa.buyornot.core.datastore import android.content.Context -import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -30,7 +29,6 @@ class UserPreferencesDataSourceImpl val ACCESS_TOKEN = stringPreferencesKey("access_token") val REFRESH_TOKEN = stringPreferencesKey("refresh_token") val USER_TYPE = stringPreferencesKey("user_type") - val IS_FIRST_RUN = booleanPreferencesKey("is_first_run") } override val preferences: Flow = @@ -48,7 +46,6 @@ class UserPreferencesDataSourceImpl UserPreferences().userType } } ?: UserPreferences().userType, - isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, ) } @@ -77,12 +74,6 @@ class UserPreferencesDataSourceImpl } } - override suspend fun updateIsFirstRun(isFirstRun: Boolean) { - context.userPreferencesDataStore.edit { prefs -> - prefs[Keys.IS_FIRST_RUN] = isFirstRun - } - } - override suspend fun updateTokens( accessToken: String, refreshToken: String, diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt index dfe991f..c8bfb0c 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt @@ -7,5 +7,4 @@ data class UserPreferences( val displayName: String, val profileImageUrl: String, val userType: UserType, - val isFirstRun: Boolean, ) diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.kt index 523241d..2b0195b 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.kt @@ -13,8 +13,18 @@ interface AppPreferencesRepository { */ val hasRequestedNotificationPermission: Flow + /** + * 최초 실행 여부를 Flow로 제공 + */ + val isFirstRun: Flow + /** * 알림 권한 요청 이력 업데이트 */ suspend fun updateNotificationPermissionRequested(requested: Boolean) + + /** + * 최초 실행 여부 업데이트 + */ + suspend fun updateIsFirstRun(isFirstRun: Boolean) } diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt index 4436dc4..aea959d 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt @@ -38,9 +38,4 @@ interface UserPreferencesRepository { * 프로필 이미지 URL 업데이트 */ suspend fun updateProfileImageUrl(newUrl: String) - - /** - * 최초 실행 여부 업데이트 - */ - suspend fun updateIsFirstRun(isFirstRun: Boolean) } From c2294732614d8115aa74ee152158ddf686429ce7 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 23:07:23 +0900 Subject: [PATCH 13/16] =?UTF-8?q?fix/#60:=20isFirstRun=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=20false=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buyornot/core/datastore/AppPreferencesDataSourceImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt index 860bc0d..0b04c0d 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt @@ -32,7 +32,7 @@ class AppPreferencesDataSourceImpl context.appPreferencesDataStore.data.map { prefs -> AppPreferences( hasRequestedNotificationPermission = prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false, - isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, + isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: false, ) } From ed859327f42146e1aceed6456b94555ec3d39962 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 23:10:15 +0900 Subject: [PATCH 14/16] =?UTF-8?q?fix/#60:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EC=8B=9C?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buyornot/core/data/repository/AuthRepositoryImpl.kt | 4 ++-- .../buyornot/core/datastore/UserPreferencesDataSource.kt | 2 +- .../buyornot/core/datastore/UserPreferencesDataSourceImpl.kt | 3 ++- .../buyornot/core/network/authenticator/TokenAuthenticator.kt | 4 ++-- .../sseotdabwa/buyornot/domain/repository/AuthRepository.kt | 2 +- .../feature/mypage/viewmodel/AccountSettingViewModel.kt | 2 +- .../buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.kt index 1452eb7..92bc5d6 100644 --- a/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.kt @@ -44,7 +44,7 @@ class AuthRepositoryImpl @Inject constructor( authApiService.logout(RefreshRequest(refreshToken)).getOrThrow() } - override suspend fun clearTokens() { - userPreferencesDataSource.clearTokens() + override suspend fun clearUserInfo() { + userPreferencesDataSource.clearUserInfo() } } diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt index a74e9dc..de0416a 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt @@ -25,5 +25,5 @@ interface UserPreferencesDataSource { suspend fun updateUserType(userType: UserType) - suspend fun clearTokens() + suspend fun clearUserInfo() } diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt index 01cfe1a..624a5c6 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt @@ -90,10 +90,11 @@ class UserPreferencesDataSourceImpl } } - override suspend fun clearTokens() { + override suspend fun clearUserInfo() { context.userPreferencesDataStore.edit { prefs -> prefs.remove(Keys.ACCESS_TOKEN) prefs.remove(Keys.REFRESH_TOKEN) + prefs.remove(Keys.PROFILE_IMAGE_URL) prefs[Keys.USER_TYPE] = UserType.GUEST.name } } diff --git a/core/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.kt b/core/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.kt index 446550d..e2cb9a5 100644 --- a/core/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.kt +++ b/core/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.kt @@ -44,7 +44,7 @@ class TokenAuthenticator @Inject constructor( val maxRetries = 2 if (retryCount(response) > maxRetries) { - userPreferencesDataSource.clearTokens() + userPreferencesDataSource.clearUserInfo() authEventBus.emit(AuthEvent.FORCE_LOGOUT) return@runBlocking null } @@ -89,7 +89,7 @@ class TokenAuthenticator @Inject constructor( .header("Authorization", "Bearer ${newTokens.accessToken}") .build() }.getOrElse { - userPreferencesDataSource.clearTokens() + userPreferencesDataSource.clearUserInfo() authEventBus.emit(AuthEvent.FORCE_LOGOUT) null } diff --git a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.kt b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.kt index eb4c6e1..1f08aa6 100644 --- a/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.kt +++ b/domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.kt @@ -7,5 +7,5 @@ interface AuthRepository { suspend fun logout() - suspend fun clearTokens() + suspend fun clearUserInfo() } diff --git a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.kt b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.kt index 2fbbdb3..3e0fae8 100644 --- a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.kt +++ b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.kt @@ -90,7 +90,7 @@ class AccountSettingViewModel @Inject constructor( // 2. (가장 중요) 어떤 경우든 로컬 토큰은 반드시 삭제 withContext(NonCancellable) { runCatchingCancellable { - authRepository.clearTokens() + authRepository.clearUserInfo() } } diff --git a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt index a0ef2c5..c10659e 100644 --- a/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt +++ b/feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt @@ -89,7 +89,7 @@ class WithdrawalViewModel @Inject constructor( // 3. 성공/실패 여부와 관계없이 항상 로컬 토큰을 삭제하고 로그인 화면으로 이동 withContext(NonCancellable) { runCatchingCancellable { - authRepository.clearTokens() + authRepository.clearUserInfo() } } From 9820d39927176ee71d5b928b4569a43b7d499379 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 23:27:03 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix/#60:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EC=8B=9C?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=A6=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buyornot/core/datastore/UserPreferencesDataSourceImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt index 624a5c6..3aa9899 100644 --- a/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt @@ -95,6 +95,7 @@ class UserPreferencesDataSourceImpl prefs.remove(Keys.ACCESS_TOKEN) prefs.remove(Keys.REFRESH_TOKEN) prefs.remove(Keys.PROFILE_IMAGE_URL) + prefs.remove(Keys.DISPLAY_NAME) prefs[Keys.USER_TYPE] = UserType.GUEST.name } } From ca97bff5344b6cd92c29f3156248f7a9f566e0ab Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Thu, 26 Feb 2026 23:30:17 +0900 Subject: [PATCH 16/16] chore: 0.0.1 (2) -> 0.0.2 (3) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0681f26..fc571ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,8 +22,8 @@ android { defaultConfig { applicationId = "com.sseotdabwa.buyornot" - versionCode = 2 - versionName = "0.0.1" + versionCode = 3 + versionName = "0.0.2" buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${localProperties.getProperty("kakao.nativeAppKey", "")}\"") manifestPlaceholders["NATIVE_APP_KEY"] = localProperties.getProperty("kakao.nativeAppKey", "")