Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e7b8949
feat: 공통 유저 세션 관리 UseCase
soochan8 Oct 21, 2025
ab6d3d3
feat: 공통 세션 State 추가
soochan8 Oct 21, 2025
28a217c
feat: ViewModel 세션 확인 로직 추가
soochan8 Oct 21, 2025
2d82c43
feat: Cart 화면 진입 시 세션 확인 로직 추가
soochan8 Oct 21, 2025
869c4b4
chore: gradle kapt plugins 수정
soochan8 Oct 21, 2025
433dca8
feat: getUserIdUseCase 추가
soochan8 Oct 21, 2025
2bfd324
feat: 사용자별 장바구니 DataStore관리 기능 추가
soochan8 Oct 21, 2025
f298b8d
refactor: CartDataStore 접근을 User기반으로 개선
soochan8 Oct 21, 2025
1a0ab4b
feat: 로그인 성공 시, 장바구니 목록 불러오도록 수정
soochan8 Oct 21, 2025
3a0a19e
feat: 로그인 성공 시, 장바구니 목록 불러오도록 수정
soochan8 Oct 21, 2025
cc695cf
feat: 로그아웃 시, 사용자 ID도 함께 제거
soochan8 Oct 21, 2025
28c9f72
feat: Cart 리다이렉트 시 popUpTo 제거 및 복귀 흐름 개선
soochan8 Oct 22, 2025
83bab2d
feat: CartDataStoreManager storeCache를 ConcurrentHashMap으로 변경
soochan8 Oct 22, 2025
4243853
feat: 로그아웃 시 CartDataStoreManager clear
soochan8 Oct 22, 2025
90a818b
feat: flowUserIdUseCase를 통해 userId 변경 시, DataStore 전환되도록 수정
soochan8 Oct 22, 2025
a9f6f29
feat: dataStoreScope Module 설정
soochan8 Oct 22, 2025
95c9189
feat: cartDataStoreManager 내부에서 외부 코루틴 스코프 사용
soochan8 Oct 22, 2025
0b1eb90
feat: 로그아웃 시, CartDataStoreManager 리소스 정리
soochan8 Oct 22, 2025
a3f0444
feat: 미사용 코루틴 스코프 Module 제거
soochan8 Oct 22, 2025
056626d
feat: UseCase 미사용 context제거
soochan8 Oct 22, 2025
bfcc7ab
change: 경로 변경, 기존 cart -> core:database
soochan8 Oct 27, 2025
7468c77
change: 경로 변경 , core:auth
soochan8 Oct 27, 2025
3677e47
chore: build dependency 추가
soochan8 Oct 27, 2025
5239f72
feat: 기본 MyPage 구성
soochan8 Oct 27, 2025
e0fda36
feat: 로그아웃 시, CartDataStoreManager.clearAll 제거
soochan8 Oct 27, 2025
e84062c
feat: 마이페이지 NavGraph 설정
soochan8 Oct 27, 2025
39c3010
change: import 경로 수정
soochan8 Oct 27, 2025
9280e92
feat: 고유 cartStore 생성
soochan8 Oct 27, 2025
613b028
feat: 로그인 NavGraph 수정
soochan8 Oct 27, 2025
bf59022
Merge pull request #64 from f-lab-edu/feature/feature#52-cart-handle-…
soochan8 Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.chan.android.state

interface SessionState {
val isSessionCheckCompleted: Boolean
}
2 changes: 2 additions & 0 deletions core/auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ android {
}

dependencies {
implementation(project(":core:database"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<UserSession?>(null)
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<String?> =
authRepository.getSessionFlow()
.map { it?.userId }
.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
20 changes: 20 additions & 0 deletions core/database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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"))

Expand All @@ -63,4 +79,8 @@ dependencies {
implementation(libs.hilt.core)

kapt(libs.hilt.compiler)

implementation(libs.datastore)
implementation(libs.datastore.proto)
implementation(libs.protobuf.javalite)
}
Original file line number Diff line number Diff line change
@@ -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<Cart> by dataStore(
val Context.cartProtoDataStore: DataStore<com.chan.cart.proto.Cart> by dataStore(
fileName = "cart.pb",
serializer = CartSerializer
)
Original file line number Diff line number Diff line change
@@ -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<String, DataStore<Cart>>()
private val scopeCache = ConcurrentHashMap<String, CoroutineScope>()

fun getDataStore(context: Context, userId: String): DataStore<Cart> {
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()
}
}
Original file line number Diff line number Diff line change
@@ -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<Cart> {
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)
}
}
Expand Down
11 changes: 9 additions & 2 deletions feature/cart/src/main/java/com/chan/cart/CartContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
}
}
}
18 changes: 18 additions & 0 deletions feature/cart/src/main/java/com/chan/cart/CartViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +16,7 @@ import javax.inject.Inject

@HiltViewModel
class CartViewModel @Inject constructor(
private val checkSessionUseCase: CheckSessionUseCase,
private val cartUseCases: CartUseCases
) : BaseViewModel<CartContract.Event, CartContract.State, CartContract.Effect>() {

Expand Down Expand Up @@ -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 }
}
}
}

Expand Down Expand Up @@ -115,6 +132,7 @@ class CartViewModel @Inject constructor(

private fun loadCartInProducts() {
viewModelScope.launch {

cartUseCases.cartItemUseCase()
.map { cartItems -> cartItems.map { it.toDataStoreCartInProductsModel() } }
.collect { products ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Cart>,
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<Cart> {
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<List<CartItem>> {
// return getCartStore().data.map { it.itemsList }
// }
//
override fun getCartItems(): Flow<List<CartItem>> {
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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -118,7 +144,7 @@ class CartRepositoryImpl @Inject constructor(
}

private suspend fun updateCartItems(transform: (List<CartItem>) -> List<CartItem>) {
dataStore.updateData { cart ->
getCartStore().updateData { cart ->
val originalItems = cart.itemsList
val updatedItems = transform(originalItems) // 로직 실행
cart.toBuilder().clearItems().addAllItems(updatedItems).build()
Expand Down
Loading