feat: 즐겨찾기 기능 프레젠테이션 레이어 추가

- MainViewModel에 즐겨찾기 토글/설정 메서드 추가
- DealItem에 즐겨찾기 버튼 및 콜백 추가
- DealListScreen에 즐겨찾기 필터 탭 추가
- 키워드 카드 색상을 옅은 붉은색으로 변경

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
sanjeok77
2026-03-05 04:15:18 +09:00
parent 9b6fc1dd01
commit bb1fbbb80c
4 changed files with 110 additions and 82 deletions

View File

@@ -32,13 +32,13 @@ import com.hotdeal.alarm.util.ShareHelper
fun DealItem( fun DealItem(
deal: HotDeal, deal: HotDeal,
onClick: () -> Unit, onClick: () -> Unit,
onFavoriteToggle: (String) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isFavorite by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
val favoriteScale by animateFloatAsState( val favoriteScale by animateFloatAsState(
targetValue = if (isFavorite) 1.2f else 1f, targetValue = if (deal.isFavorite) 1.2f else 1f,
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow stiffness = Spring.StiffnessLow
@@ -205,15 +205,15 @@ fun DealItem(
// 즐겨찾기 버튼 // 즐겨찾기 버튼
IconButton( IconButton(
onClick = { isFavorite = !isFavorite }, onClick = { onFavoriteToggle(deal.id) },
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.scale(favoriteScale) .scale(favoriteScale)
) { ) {
Icon( Icon(
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, imageVector = if (deal.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = if (isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가", contentDescription = if (deal.isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
tint = if (isFavorite) tint = if (deal.isFavorite)
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else else
MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant,

View File

@@ -42,6 +42,7 @@ fun DealListScreen(
var searchText by remember { mutableStateOf("") } var searchText by remember { mutableStateOf("") }
var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) } var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) }
var showFavoritesOnly by remember { mutableStateOf(false) }
var showFilterMenu by remember { mutableStateOf(false) } var showFilterMenu by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
@@ -161,6 +162,14 @@ fun DealListScreen(
color = color color = color
) )
} }
// 즐겨찾기 필터
EnhancedFilterChip(
selected = showFavoritesOnly,
onClick = { showFavoritesOnly = !showFavoritesOnly },
label = "즐겨찾기",
color = MaterialTheme.colorScheme.error
)
} }
} }
} }
@@ -210,13 +219,14 @@ fun DealListScreen(
} }
is MainUiState.Success -> { is MainUiState.Success -> {
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter) { val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly) {
state.deals.filter { deal -> state.deals.filter { deal ->
val matchesSearch = searchText.isBlank() || val matchesSearch = searchText.isBlank() ||
deal.title.contains(searchText, ignoreCase = true) deal.title.contains(searchText, ignoreCase = true)
val matchesSite = selectedSiteFilter == null || val matchesSite = selectedSiteFilter == null ||
deal.siteType == selectedSiteFilter deal.siteType == selectedSiteFilter
matchesSearch && matchesSite val matchesFavorite = !showFavoritesOnly || deal.isFavorite
matchesSearch && matchesSite && matchesFavorite
} }
} }
@@ -294,6 +304,9 @@ fun DealListScreen(
onClick = { onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
context.startActivity(intent) context.startActivity(intent)
},
onFavoriteToggle = { dealId ->
viewModel.toggleFavorite(dealId)
} }
) )
} }

View File

@@ -108,6 +108,24 @@ class MainViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
keywordDao.updateEnabled(id, enabled) keywordDao.updateEnabled(id, enabled)
} }
}
/**
* 즐겨찾기 토글
*/
fun toggleFavorite(dealId: String) {
viewModelScope.launch {
hotDealDao.toggleFavorite(dealId)
}
}
/**
* 즐겨찾기 설정
*/
fun setFavorite(dealId: String, isFavorite: Boolean) {
viewModelScope.launch {
hotDealDao.setFavorite(dealId, isFavorite)
}
} }
fun refresh() { fun refresh() {

View File

@@ -754,10 +754,7 @@ private fun EnhancedKeywordCard(
Box( Box(
modifier = Modifier.size(40.dp) modifier = Modifier.size(40.dp)
.background( .background(
if (keyword.isEnabled) Color(0xFFFFCDD2), // 옅은 붉은색 (Light Red)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
CircleShape CircleShape
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -765,7 +762,7 @@ private fun EnhancedKeywordCard(
Icon( Icon(
imageVector = Icons.Filled.Tag, imageVector = Icons.Filled.Tag,
contentDescription = null, contentDescription = null,
tint = Color.White, tint = Color(0xFFD32F2F), // 붉은색 아이콘
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
} }