Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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.like.proto.Like
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.util.concurrent.ConcurrentHashMap

object LikeDataStoreManager {

private val storeCache = ConcurrentHashMap<String, DataStore<Like>>()
private val scopeCache = ConcurrentHashMap<String, CoroutineScope>()

fun getDataStore(context: Context, userId: String): DataStore<Like> {

return storeCache.getOrPut(userId) {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scopeCache[userId] = scope

DataStoreFactory.create(
serializer = LikeSerializer,
produceFile = { context.dataStoreFile("like_$userId.pb") },
scope = scope
)
}
}

fun clearAll() {
scopeCache.values.forEach { it.cancel() }
scopeCache.clear()
storeCache.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.chan.database.datastore

import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.chan.like.proto.Like
import java.io.InputStream
import java.io.OutputStream

object LikeSerializer : Serializer<Like> {
override val defaultValue: Like = Like.getDefaultInstance()

override suspend fun readFrom(input: InputStream): Like {
try {
return Like.parseFrom(input)
} catch (exception: com.google.protobuf.InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}

override suspend fun writeTo(t: Like, output: OutputStream) = t.writeTo(output)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.chan.database.di

import com.chan.database.repository.LikeRepositoryImpl
import com.chan.domain.repository.LikeRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

@Binds
abstract fun bindLikeRepository(
likeRepositoryImpl: LikeRepositoryImpl
) : LikeRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.chan.database.mappers

import com.chan.database.entity.CommonProductEntity
import com.chan.domain.ProductsVO

fun CommonProductEntity.toProductsVO(): ProductsVO {
return ProductsVO(
productId = productId,
productName = productName,
brandName = brandName,
imageUrl = imageUrl,
originalPrice = originalPrice,
discountPercent = discountPercent,
discountPrice = discountPrice,
tags = tags,
reviewRating = reviewRating,
reviewCount = reviewCount,
categoryIds = categoryIds,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.chan.database.repository

import android.content.Context
import androidx.datastore.core.DataStore
import com.chan.auth.domain.usecase.GetCurrentUserIdUseCase
import com.chan.database.dao.ProductsDao
import com.chan.database.datastore.LikeDataStoreManager
import com.chan.database.mappers.toProductsVO
import com.chan.domain.ProductsVO
import com.chan.domain.repository.LikeRepository
import com.chan.like.proto.Like
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
import kotlin.collections.toMutableSet
import kotlin.collections.toSet

class LikeRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val getCurrentUserIdUseCase: GetCurrentUserIdUseCase,
private val productsDao: ProductsDao
) : LikeRepository {

private fun getLikeStore(): DataStore<Like> {
val userId = getCurrentUserIdUseCase() ?: "guest"
return LikeDataStoreManager.getDataStore(context, userId)
}


override fun getLikedProductIds(): Flow<Set<String>> {
return getLikeStore().data
.map { it.productIdsList.toSet() }
.distinctUntilChanged()
}

override suspend fun toggleLike(productId: String) {
getLikeStore().updateData { current ->
val currentLikedSet = current.productIdsList.toMutableSet()

if (currentLikedSet.contains(productId))
currentLikedSet.remove(productId)
else
currentLikedSet.add(productId)

current.toBuilder().clearProductIds().addAllProductIds(currentLikedSet).build()
}
}

override fun getLikedProducts(): Flow<List<ProductsVO>> {
return getLikeStore().data.mapLatest { likeProto ->
val likedIds = likeProto.productIdsList.toSet()
if (likedIds.isEmpty())
return@mapLatest emptyList()

val allProducts = productsDao.getAllProducts()

allProducts
.filter { it.productId in likedIds }
.map { it.toProductsVO() }

}
}

}
8 changes: 8 additions & 0 deletions core/database/src/main/proto/like.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
syntax = "proto3";

option java_package = "com.chan.like.proto";
option java_multiple_files = true;

message Like {
repeated string product_ids = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.chan.domain.repository

import com.chan.domain.ProductsVO
import kotlinx.coroutines.flow.Flow


interface LikeRepository {
fun getLikedProductIds() : Flow<Set<String>>
suspend fun toggleLike(productId: String)
fun getLikedProducts() : Flow<List<ProductsVO>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.chan.domain.usecase

import com.chan.domain.repository.LikeRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GetLikedProductIdsUseCase @Inject constructor(
private val likeRepository: LikeRepository,
) {
operator fun invoke() : Flow<Set<String>> {
return likeRepository.getLikedProductIds()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.chan.domain.usecase

import com.chan.domain.ProductsVO
import com.chan.domain.repository.LikeRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GetLikedProductsUseCase @Inject constructor(
private val likeRepository: LikeRepository
) {
operator fun invoke() : Flow<List<ProductsVO>> {
return likeRepository.getLikedProducts()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.chan.domain.usecase

import com.chan.domain.repository.LikeRepository
import javax.inject.Inject

class ToggleLikeUseCase @Inject constructor(
private val likeRepository: LikeRepository
) {
suspend operator fun invoke(productId: String) {
likeRepository.toggleLike(productId)
}
}
42 changes: 42 additions & 0 deletions feature/like/src/main/java/com/chan/like/LikeContract.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.chan.like

import com.chan.android.LoadingState
import com.chan.android.ViewEffect
import com.chan.android.ViewEvent
import com.chan.android.ViewState
import com.chan.android.model.ProductsModel

class LikeContract {

sealed class Event : ViewEvent {
object LoadFavorites: Event()

sealed class TopBar : Event() {
object NavigateToSearch : TopBar()
object NavigateToCart : TopBar()
}

sealed class LikeAction : Event() {
data class ToggleFavorite(val productId: String) : LikeAction()
data class NavigateToProductDetail(val productId: String) : LikeAction()
data class ShowCartPopup(val productId: String) : LikeAction()
}
}

data class State(
//좋아요 상품 목록
val likeProducts: List<ProductsModel> = emptyList(),
//좋아요 상품 Id
val likedProductIds: Set<String> = emptySet(),
val loadingState: LoadingState = LoadingState.Idle
) : ViewState

sealed class Effect : ViewEffect {
sealed class Navigation : Effect() {
object ToCartRoute : Navigation()
object ToSearchRoute : Navigation()
data class ToCartPopupRoute(val productId: String) : Navigation()
data class ToProductDetail(val productId: String) : Navigation()
}
}
}
73 changes: 73 additions & 0 deletions feature/like/src/main/java/com/chan/like/LikeScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.chan.like

import androidx.compose.foundation.background
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.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.chan.android.model.ProductCardOrientation
import com.chan.android.ui.CommonProductsCard
import com.chan.android.ui.composable.MainTopBar
import com.chan.android.ui.theme.White

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LikeScreen(
state: LikeContract.State,
onEvent: (LikeContract.Event) -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()

Scaffold(
topBar = {
Column {
MainTopBar(
navigationIcon = null,
titleContent = {
Text(text = stringResource(R.string.history))
},
actions = {
IconButton(onClick = { onEvent(LikeContract.Event.TopBar.NavigateToSearch) }) {
Icon(Icons.Default.Search, contentDescription = "검색")
}
IconButton(onClick = { onEvent(LikeContract.Event.TopBar.NavigateToCart) }) {
Icon(Icons.Default.ShoppingCart, contentDescription = "장바구니")
}
},
scrollBehavior = scrollBehavior
)
}
},
modifier = Modifier.fillMaxSize()
) { innerPadding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.background(White)) {
state.likeProducts.forEach { product ->
CommonProductsCard(
product = product,
isLiked = product.productId in state.likedProductIds,
modifier = Modifier.fillMaxWidth(),
orientation = ProductCardOrientation.HORIZONTAL,
onClick = { onEvent(LikeContract.Event.LikeAction.NavigateToProductDetail(it)) },
onLikeClick = { onEvent(LikeContract.Event.LikeAction.ToggleFavorite(it)) },
onCartClick = { onEvent(LikeContract.Event.LikeAction.ShowCartPopup(it)) }
)
}

}
}
}
Loading