diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0de82ac0..246c1d7d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.hilt) alias(libs.plugins.google.services) - kotlin("kapt") + alias(libs.plugins.kotlin.kapt) } android { diff --git a/build.gradle.kts b/build.gradle.kts index 3c45f039..d710471c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,11 +2,11 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.kapt) apply false // alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.jetbrains.kotlin.jvm) apply false - alias(libs.plugins.kotlin.kapt) apply false } diff --git a/core/android/src/main/java/com/chan/android/state/SessionState.kt b/core/android/src/main/java/com/chan/android/state/SessionState.kt new file mode 100644 index 00000000..dfa30dbd --- /dev/null +++ b/core/android/src/main/java/com/chan/android/state/SessionState.kt @@ -0,0 +1,5 @@ +package com.chan.android.state + +interface SessionState { + val isSessionCheckCompleted: Boolean +} \ No newline at end of file diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts index 66694765..abf5df9a 100644 --- a/core/auth/build.gradle.kts +++ b/core/auth/build.gradle.kts @@ -35,6 +35,8 @@ android { } dependencies { + implementation(project(":core:database")) + implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) diff --git a/core/auth/src/main/java/com/chan/auth/data/AuthRepositoryImpl.kt b/core/auth/src/main/java/com/chan/auth/data/AuthRepositoryImpl.kt index 270dae16..43f0b7c1 100644 --- a/core/auth/src/main/java/com/chan/auth/data/AuthRepositoryImpl.kt +++ b/core/auth/src/main/java/com/chan/auth/data/AuthRepositoryImpl.kt @@ -3,6 +3,7 @@ package com.chan.auth.data import android.content.SharedPreferences import com.chan.auth.domain.AuthRepository import com.chan.auth.domain.UserSession +import com.chan.database.datastore.CartDataStoreManager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -11,7 +12,7 @@ import javax.inject.Singleton @Singleton class AuthRepositoryImpl @Inject constructor( - private val prefs: SharedPreferences + private val prefs: SharedPreferences, ) : AuthRepository { private val _sessionFlow = MutableStateFlow(null) @@ -40,7 +41,7 @@ class AuthRepositoryImpl @Inject constructor( } override suspend fun logout() { - prefs.edit().remove(KEY_TOKEN).apply() + prefs.edit().remove(KEY_USER_ID).remove(KEY_TOKEN).apply() _sessionFlow.value = null } diff --git a/core/auth/src/main/java/com/chan/auth/domain/usecase/CheckSessionUseCase.kt b/core/auth/src/main/java/com/chan/auth/domain/usecase/CheckSessionUseCase.kt new file mode 100644 index 00000000..5702f43c --- /dev/null +++ b/core/auth/src/main/java/com/chan/auth/domain/usecase/CheckSessionUseCase.kt @@ -0,0 +1,13 @@ +package com.chan.auth.domain.usecase + +import com.chan.auth.domain.AuthRepository +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +class CheckSessionUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke() : Boolean { + return authRepository.getSessionFlow().firstOrNull() != null + } +} \ No newline at end of file diff --git a/core/auth/src/main/java/com/chan/auth/domain/usecase/FlowCurrentUserIdUseCase.kt b/core/auth/src/main/java/com/chan/auth/domain/usecase/FlowCurrentUserIdUseCase.kt new file mode 100644 index 00000000..40eb3ee4 --- /dev/null +++ b/core/auth/src/main/java/com/chan/auth/domain/usecase/FlowCurrentUserIdUseCase.kt @@ -0,0 +1,17 @@ +package com.chan.auth.domain.usecase + +import com.chan.auth.domain.AuthRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +// Flow 구독용 +class FlowCurrentUserIdUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + operator fun invoke(): Flow = + authRepository.getSessionFlow() + .map { it?.userId } + .distinctUntilChanged() +} \ No newline at end of file diff --git a/core/auth/src/main/java/com/chan/auth/domain/usecase/GetCurrentUserIdUseCase.kt b/core/auth/src/main/java/com/chan/auth/domain/usecase/GetCurrentUserIdUseCase.kt new file mode 100644 index 00000000..f91ca32e --- /dev/null +++ b/core/auth/src/main/java/com/chan/auth/domain/usecase/GetCurrentUserIdUseCase.kt @@ -0,0 +1,12 @@ +package com.chan.auth.domain.usecase + +import com.chan.auth.domain.AuthRepository +import javax.inject.Inject + +class GetCurrentUserIdUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + operator fun invoke() : String? { + return authRepository.getCurrentUserId() + } +} \ No newline at end of file diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index fec0dce4..688b635a 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.hilt) + alias(libs.plugins.protobuf) kotlin("kapt") } @@ -39,6 +40,21 @@ android { } } +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}" + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") // Android는 lite 필수 + } + } + } + } +} + dependencies { implementation(project(":core:domain")) @@ -63,4 +79,8 @@ dependencies { implementation(libs.hilt.core) kapt(libs.hilt.compiler) + + implementation(libs.datastore) + implementation(libs.datastore.proto) + implementation(libs.protobuf.javalite) } \ No newline at end of file diff --git a/feature/cart/src/main/java/com/chan/cart/data/datastore/CartDataStore.kt b/core/database/src/main/java/com/chan/database/datastore/CartDataStore.kt similarity index 56% rename from feature/cart/src/main/java/com/chan/cart/data/datastore/CartDataStore.kt rename to core/database/src/main/java/com/chan/database/datastore/CartDataStore.kt index 6d88a026..fb369230 100644 --- a/feature/cart/src/main/java/com/chan/cart/data/datastore/CartDataStore.kt +++ b/core/database/src/main/java/com/chan/database/datastore/CartDataStore.kt @@ -1,11 +1,10 @@ -package com.chan.cart.data.datastore +package com.chan.database.datastore import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.dataStore -import com.chan.cart.proto.Cart -val Context.cartProtoDataStore: DataStore by dataStore( +val Context.cartProtoDataStore: DataStore by dataStore( fileName = "cart.pb", serializer = CartSerializer ) diff --git a/core/database/src/main/java/com/chan/database/datastore/CartDataStoreManager.kt b/core/database/src/main/java/com/chan/database/datastore/CartDataStoreManager.kt new file mode 100644 index 00000000..e886b75d --- /dev/null +++ b/core/database/src/main/java/com/chan/database/datastore/CartDataStoreManager.kt @@ -0,0 +1,40 @@ +package com.chan.database.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.chan.cart.proto.Cart +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Singleton +import kotlin.collections.getOrPut + +@Singleton +object CartDataStoreManager { + + private val storeCache = ConcurrentHashMap>() + private val scopeCache = ConcurrentHashMap() + + fun getDataStore(context: Context, userId: String): DataStore { + return storeCache.getOrPut(userId) { + val userScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + scopeCache[userId] = userScope + + DataStoreFactory.create( + serializer = CartSerializer, + produceFile = { context.dataStoreFile("cart_$userId.pb") }, + scope = userScope + ) + } + } + + fun clearAll() { + scopeCache.values.forEach { it.cancel() } + scopeCache.clear() + storeCache.clear() + } +} \ No newline at end of file diff --git a/feature/cart/src/main/java/com/chan/cart/data/datastore/CartSerializer.kt b/core/database/src/main/java/com/chan/database/datastore/CartSerializer.kt similarity index 70% rename from feature/cart/src/main/java/com/chan/cart/data/datastore/CartSerializer.kt rename to core/database/src/main/java/com/chan/database/datastore/CartSerializer.kt index 646e66dc..f8160671 100644 --- a/feature/cart/src/main/java/com/chan/cart/data/datastore/CartSerializer.kt +++ b/core/database/src/main/java/com/chan/database/datastore/CartSerializer.kt @@ -1,19 +1,19 @@ -package com.chan.cart.data.datastore +package com.chan.database.datastore import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer import com.chan.cart.proto.Cart -import com.google.protobuf.InvalidProtocolBufferException import java.io.InputStream import java.io.OutputStream object CartSerializer : Serializer { - override val defaultValue: Cart = Cart.getDefaultInstance() + override val defaultValue: Cart = + Cart.getDefaultInstance() override suspend fun readFrom(input: InputStream): Cart { try { return Cart.parseFrom(input) - } catch (exception: InvalidProtocolBufferException) { + } catch (exception: com.google.protobuf.InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } diff --git a/feature/cart/src/main/proto/cart.proto b/core/database/src/main/proto/cart.proto similarity index 100% rename from feature/cart/src/main/proto/cart.proto rename to core/database/src/main/proto/cart.proto diff --git a/feature/cart/src/main/java/com/chan/cart/CartContract.kt b/feature/cart/src/main/java/com/chan/cart/CartContract.kt index 9cda451a..3a97f8d1 100644 --- a/feature/cart/src/main/java/com/chan/cart/CartContract.kt +++ b/feature/cart/src/main/java/com/chan/cart/CartContract.kt @@ -5,6 +5,7 @@ import com.chan.android.LoadingState import com.chan.android.ViewEffect import com.chan.android.ViewEvent import com.chan.android.ViewState +import com.chan.android.state.SessionState import com.chan.cart.model.CartInProductsModel import com.chan.cart.model.CartInTobBarModel import com.chan.cart.model.PopupProductInfoModel @@ -14,6 +15,7 @@ class CartContract { data class SelectedTab(val index: Int) : Event() data class LoadPopupProductInfo(val productId: String) : Event() object LoadCartProducts : Event() + object CheckUserSession : Event() data class AddToProduct(val productId: String) : Event() data class UpdateProductSelected(val productId: String, val isSelected: Boolean) : Event() data class UpdateProductQuantity(val productId: String, val isAdd: Boolean) : Event() @@ -29,12 +31,17 @@ class CartContract { val totalProductsCount : Int = 0, val totalPrice : Int = 0, val allSelected : Boolean = false, - val loadingState: LoadingState = LoadingState.Idle - ) : ViewState + val loadingState: LoadingState = LoadingState.Idle, + override val isSessionCheckCompleted: Boolean = false + ) : ViewState, SessionState sealed class Effect : ViewEffect { data class ShowError(val errorMsg: String) : Effect() data class ShowToast(@StringRes val message: Int) : Effect() object DismissCartPopup : Effect() + + sealed class Navigation : Effect() { + object ToLogin : Navigation() + } } } \ No newline at end of file diff --git a/feature/cart/src/main/java/com/chan/cart/CartViewModel.kt b/feature/cart/src/main/java/com/chan/cart/CartViewModel.kt index 83933305..40bc2c1d 100644 --- a/feature/cart/src/main/java/com/chan/cart/CartViewModel.kt +++ b/feature/cart/src/main/java/com/chan/cart/CartViewModel.kt @@ -3,6 +3,8 @@ package com.chan.cart import androidx.lifecycle.viewModelScope import com.chan.android.BaseViewModel import com.chan.android.LoadingState +import com.chan.auth.domain.usecase.CheckSessionUseCase +import com.chan.cart.CartContract.Effect.Navigation.ToLogin import com.chan.cart.domain.usecase.CartUseCases import com.chan.cart.model.CartInTobBarModel import com.chan.cart.ui.mapper.toDataStoreCartInProductsModel @@ -14,6 +16,7 @@ import javax.inject.Inject @HiltViewModel class CartViewModel @Inject constructor( + private val checkSessionUseCase: CheckSessionUseCase, private val cartUseCases: CartUseCases ) : BaseViewModel() { @@ -42,6 +45,20 @@ class CartViewModel @Inject constructor( CartContract.Event.OnAllSelected -> updateAllSelected() is CartContract.Event.DeleteProduct -> deleteProduct(event.productId) + CartContract.Event.CheckUserSession -> checkSessionStatus() + } + } + + private fun checkSessionStatus() { + viewModelScope.launch { + val currentSession = checkSessionUseCase.invoke() + + if (currentSession) { + setState { copy(isSessionCheckCompleted = true) } + setEvent(CartContract.Event.LoadCartProducts) + } else { + setEffect { ToLogin } + } } } @@ -115,6 +132,7 @@ class CartViewModel @Inject constructor( private fun loadCartInProducts() { viewModelScope.launch { + cartUseCases.cartItemUseCase() .map { cartItems -> cartItems.map { it.toDataStoreCartInProductsModel() } } .collect { products -> diff --git a/feature/cart/src/main/java/com/chan/cart/data/CartRepositoryImpl.kt b/feature/cart/src/main/java/com/chan/cart/data/CartRepositoryImpl.kt index 84e43703..42d5e54d 100644 --- a/feature/cart/src/main/java/com/chan/cart/data/CartRepositoryImpl.kt +++ b/feature/cart/src/main/java/com/chan/cart/data/CartRepositoryImpl.kt @@ -1,34 +1,60 @@ package com.chan.cart.data +import android.content.Context import androidx.datastore.core.DataStore +import com.chan.auth.domain.usecase.FlowCurrentUserIdUseCase +import com.chan.auth.domain.usecase.GetCurrentUserIdUseCase +import com.chan.database.datastore.CartDataStoreManager import com.chan.cart.data.mapper.toProductsVO import com.chan.cart.domain.CartRepository import com.chan.cart.proto.Cart import com.chan.cart.proto.CartItem import com.chan.database.dao.ProductsDao import com.chan.domain.ProductsVO +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton @Singleton class CartRepositoryImpl @Inject constructor( - private val dataStore: DataStore, - private val productsDao: ProductsDao + @ApplicationContext private val context: Context, + private val productsDao: ProductsDao, + private val getCurrentUserIdUseCase: GetCurrentUserIdUseCase, + private val flowCurrentUserIdUseCase: FlowCurrentUserIdUseCase ) : CartRepository { + private fun getCartStore(): DataStore { + val userId = getCurrentUserIdUseCase() ?: "guest" + return CartDataStoreManager.getDataStore(context, userId) + } + override suspend fun getProductInfo(productId: String): ProductsVO { return productsDao.getProductsByProductId(productId)?.toProductsVO() ?: throw NoSuchElementException("Product not found with id: $productId") } +// override fun getCartItems(): Flow> { +// return getCartStore().data.map { it.itemsList } +// } +// override fun getCartItems(): Flow> { - return dataStore.data.map { it.itemsList } + return flowCurrentUserIdUseCase() + .map { it ?: "guest" } + .flatMapLatest { userId -> + CartDataStoreManager + .getDataStore(context, userId) + .data + .map { it.itemsList } + } } + + override suspend fun addProductToCart(productId: String) { - dataStore.updateData { cart -> + getCartStore().updateData { cart -> val existingItem = cart.itemsList.find { it.productId == productId } if (existingItem != null) { @@ -91,7 +117,7 @@ class CartRepositoryImpl @Inject constructor( } override suspend fun decreaseProductQuantity(productId: String) { - dataStore.updateData { cart -> + getCartStore().updateData { cart -> val targetItem = cart.itemsList.find { it.productId == productId } ?: return@updateData cart val updatedItems = if (targetItem.quantity > 1) { @@ -118,7 +144,7 @@ class CartRepositoryImpl @Inject constructor( } private suspend fun updateCartItems(transform: (List) -> List) { - dataStore.updateData { cart -> + getCartStore().updateData { cart -> val originalItems = cart.itemsList val updatedItems = transform(originalItems) // 로직 실행 cart.toBuilder().clearItems().addAllItems(updatedItems).build() diff --git a/feature/cart/src/main/java/com/chan/cart/data/di/DataStoreModule.kt b/feature/cart/src/main/java/com/chan/cart/data/di/DataStoreModule.kt index cabff8c0..7f040b45 100644 --- a/feature/cart/src/main/java/com/chan/cart/data/di/DataStoreModule.kt +++ b/feature/cart/src/main/java/com/chan/cart/data/di/DataStoreModule.kt @@ -2,7 +2,7 @@ package com.chan.cart.data.di import android.content.Context import androidx.datastore.core.DataStore -import com.chan.cart.data.datastore.cartProtoDataStore +import com.chan.database.datastore.cartProtoDataStore import com.chan.cart.proto.Cart import dagger.Module import dagger.Provides diff --git a/feature/cart/src/main/java/com/chan/cart/naivgation/CartNavGraph.kt b/feature/cart/src/main/java/com/chan/cart/naivgation/CartNavGraph.kt index 2d4f47cd..7b71cc42 100644 --- a/feature/cart/src/main/java/com/chan/cart/naivgation/CartNavGraph.kt +++ b/feature/cart/src/main/java/com/chan/cart/naivgation/CartNavGraph.kt @@ -16,6 +16,9 @@ import com.chan.cart.CartViewModel import com.chan.cart.ui.CartScreen import com.chan.cart.ui.popup.CartPopupScreen import com.chan.navigation.NavGraphProvider +import com.chan.navigation.Routes +import com.chan.navigation.createLoginRoute +import kotlinx.coroutines.flow.filterIsInstance import javax.inject.Inject class CartNavGraph @Inject constructor() : NavGraphProvider { @@ -41,16 +44,32 @@ class CartNavGraph @Inject constructor() : NavGraphProvider { fun CartRoute(navController: NavHostController) { val viewModel: CartViewModel = hiltViewModel() val state by viewModel.viewState.collectAsState() - val context = LocalContext.current LaunchedEffect(Unit) { - viewModel.setEvent(CartContract.Event.LoadCartProducts) + viewModel.effect + .filterIsInstance() + .collect { navigate -> + when (navigate) { + CartContract.Effect.Navigation.ToLogin -> { + navController.navigate(createLoginRoute(Routes.CART.route)) { + launchSingleTop = true + restoreState = true + } + } + } + } } - CartScreen( - state = state, - onEvent = viewModel::setEvent - ) + LaunchedEffect(Unit) { + viewModel.setEvent(CartContract.Event.CheckUserSession) + } + + if (state.isSessionCheckCompleted) { + CartScreen( + state = state, + onEvent = viewModel::setEvent + ) + } } @@ -60,24 +79,39 @@ fun CartPopupRoute(navController: NavHostController, productId: String) { val state by viewModel.viewState.collectAsState() val context = LocalContext.current + LaunchedEffect(Unit) { + viewModel.effect + .collect { effect -> + when (effect) { + is CartContract.Effect.ShowToast -> { + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + } + + is CartContract.Effect.ShowError -> { + Toast.makeText(context, effect.errorMsg, Toast.LENGTH_SHORT).show() + } + + CartContract.Effect.DismissCartPopup -> { + navController.popBackStack() + } + + CartContract.Effect.Navigation.ToLogin -> { + navController.navigate(createLoginRoute(Routes.HOME.route)) { + popUpTo(Routes.CART.route) { + inclusive = true + } + } + } + } + } + } + LaunchedEffect(key1 = productId) { viewModel.handleEvent(CartContract.Event.LoadPopupProductInfo(productId)) } LaunchedEffect(Unit) { - viewModel.effect.collect { effect -> - when (effect) { - is CartContract.Effect.ShowToast -> { - Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() - } - - is CartContract.Effect.ShowError -> { - Toast.makeText(context, effect.errorMsg, Toast.LENGTH_SHORT).show() - } - - CartContract.Effect.DismissCartPopup -> navController.popBackStack() - } - } + viewModel.setEvent(CartContract.Event.CheckUserSession) } CartPopupScreen( diff --git a/feature/login/src/main/java/com/chan/login/navigation/LoginNavGraph.kt b/feature/login/src/main/java/com/chan/login/navigation/LoginNavGraph.kt index da93fda4..54ea0ffb 100644 --- a/feature/login/src/main/java/com/chan/login/navigation/LoginNavGraph.kt +++ b/feature/login/src/main/java/com/chan/login/navigation/LoginNavGraph.kt @@ -40,15 +40,12 @@ fun LoginRoute(navController: NavHostController, redirectRoute: String) { val state by viewModel.viewState.collectAsState() val context = LocalContext.current - LaunchedEffect(Unit) { - viewModel.setEvent(LoginContract.Event.CheckUserSession) - } - LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { LoginContract.Effect.NavigateToHome -> { - val target = if (redirectRoute.isNotEmpty()) redirectRoute else Routes.MYPAGE.route + val target = + if (redirectRoute.isNotEmpty()) redirectRoute else Routes.MYPAGE.route navController.navigate(target) { popUpTo(Routes.LOGIN.route) { inclusive = true } } @@ -62,9 +59,15 @@ fun LoginRoute(navController: NavHostController, redirectRoute: String) { } } - if (state.isSessionCheckCompleted) + LaunchedEffect(Unit) { + viewModel.setEvent(LoginContract.Event.CheckUserSession) + } + + + if (state.isSessionCheckCompleted) { LoginScreen( state = state, onEvent = viewModel::setEvent ) + } } \ No newline at end of file diff --git a/feature/mypage/build.gradle.kts b/feature/mypage/build.gradle.kts index de52f521..874a12e5 100644 --- a/feature/mypage/build.gradle.kts +++ b/feature/mypage/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(project(":core:android")) implementation(project(":core:database")) implementation(project(":core:navigation")) + implementation(project(":core:auth")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) diff --git a/feature/mypage/src/main/java/com/chan/mypage/MyPageContract.kt b/feature/mypage/src/main/java/com/chan/mypage/MyPageContract.kt new file mode 100644 index 00000000..b6b67e53 --- /dev/null +++ b/feature/mypage/src/main/java/com/chan/mypage/MyPageContract.kt @@ -0,0 +1,28 @@ +package com.chan.mypage + +import com.chan.android.ViewEffect +import com.chan.android.ViewEvent +import com.chan.android.ViewState + +class MyPageContract { + + sealed class Event: ViewEvent { + object OnLogoutClicked : Event() + } + + data class State( + val userState: UserState = UserState() + ) : ViewState + + data class UserState ( + val userName: String? = null, + val userId: String? = null + ) + + sealed class Effect: ViewEffect { + data class ShowToast(val message: String) : Effect() + sealed class Navigation : Effect() { + object ToHome : Navigation() + } + } +} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/chan/mypage/MyPageScreen.kt b/feature/mypage/src/main/java/com/chan/mypage/MyPageScreen.kt index 93faa56a..ead29cfe 100644 --- a/feature/mypage/src/main/java/com/chan/mypage/MyPageScreen.kt +++ b/feature/mypage/src/main/java/com/chan/mypage/MyPageScreen.kt @@ -1,9 +1,37 @@ package com.chan.mypage +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.chan.android.ui.theme.Spacing +import com.chan.android.ui.theme.White @Composable -fun MyPageScreen() { - Text(text = "마이 페이지 입니다.") +fun MyPageScreen( + state: MyPageContract.State, + onEvent: (MyPageContract.Event) -> Unit +) { + Column( modifier = Modifier + .fillMaxSize() + .background(White) + .padding(Spacing.spacing4), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "마이 페이지 입니다.") + + Button( + modifier = Modifier.fillMaxWidth().padding(Spacing.spacing4), + onClick = { onEvent(MyPageContract.Event.OnLogoutClicked) }) { + Text("로그아웃") + } + } } \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/chan/mypage/MyPageViewModel.kt b/feature/mypage/src/main/java/com/chan/mypage/MyPageViewModel.kt new file mode 100644 index 00000000..695a679a --- /dev/null +++ b/feature/mypage/src/main/java/com/chan/mypage/MyPageViewModel.kt @@ -0,0 +1,29 @@ +package com.chan.mypage + +import androidx.lifecycle.viewModelScope +import com.chan.android.BaseViewModel +import com.chan.mypage.MyPageContract.Effect.Navigation.ToHome +import com.chan.mypage.domain.usecase.LogoutUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MyPageViewModel @Inject constructor( + private val logoutUseCase: LogoutUseCase +) : BaseViewModel() { + override fun setInitialState() = MyPageContract.State() + + override fun handleEvent(event: MyPageContract.Event) { + when (event) { + MyPageContract.Event.OnLogoutClicked -> logout() + } + } + + private fun logout() { + viewModelScope.launch { + logoutUseCase() + setEffect { ToHome } + } + } +} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/chan/mypage/domain/usecase/LogoutUseCase.kt b/feature/mypage/src/main/java/com/chan/mypage/domain/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..6ea3d714 --- /dev/null +++ b/feature/mypage/src/main/java/com/chan/mypage/domain/usecase/LogoutUseCase.kt @@ -0,0 +1,16 @@ +package com.chan.mypage.domain.usecase + +import android.content.Context +import com.chan.auth.domain.AuthRepository +import com.chan.database.datastore.CartDataStoreManager +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke() { + authRepository.logout() + CartDataStoreManager.clearAll() + } +} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/chan/mypage/navigation/MyPageNavGraph.kt b/feature/mypage/src/main/java/com/chan/mypage/navigation/MyPageNavGraph.kt index cc2fb65b..19bfc4fa 100644 --- a/feature/mypage/src/main/java/com/chan/mypage/navigation/MyPageNavGraph.kt +++ b/feature/mypage/src/main/java/com/chan/mypage/navigation/MyPageNavGraph.kt @@ -1,10 +1,20 @@ package com.chan.mypage.navigation +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import com.chan.mypage.MyPageContract import com.chan.mypage.MyPageScreen +import com.chan.mypage.MyPageViewModel import com.chan.navigation.NavGraphProvider +import com.chan.navigation.Routes import javax.inject.Inject class MyPageNavGraph @Inject constructor() : NavGraphProvider { @@ -13,7 +23,34 @@ class MyPageNavGraph @Inject constructor() : NavGraphProvider { navController: NavHostController ) { navGraphBuilder.composable(MyPageDestination.route) { - MyPageScreen() + MyPageRoute(navController) } } +} + + +@Composable +fun MyPageRoute(navController: NavHostController) { + val viewModel: MyPageViewModel = hiltViewModel() + val state by viewModel.viewState.collectAsStateWithLifecycle() + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when(effect) { + MyPageContract.Effect.Navigation.ToHome -> { + navController.navigate(Routes.HOME.route) { + popUpTo(0) + launchSingleTop = true + } + } + is MyPageContract.Effect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + } + } + } + + MyPageScreen( + state = state, + onEvent = viewModel::setEvent + ) } \ No newline at end of file