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

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

View File

@@ -40,9 +40,10 @@ fun DealListScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
var searchText by remember { mutableStateOf("") }
var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) }
var showFilterMenu by remember { mutableStateOf(false) }
var searchText by remember { mutableStateOf("") }
var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) }
var showFavoritesOnly by remember { mutableStateOf(false) }
var showFilterMenu by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
@@ -150,17 +151,25 @@ fun DealListScreen(
color = MaterialTheme.colorScheme.primary
)
SiteType.entries.forEach { siteType ->
val color = getSiteColor(siteType)
EnhancedFilterChip(
selected = selectedSiteFilter == siteType,
onClick = {
selectedSiteFilter = if (selectedSiteFilter == siteType) null else siteType
},
label = siteType.displayName,
color = color
)
}
SiteType.entries.forEach { siteType ->
val color = getSiteColor(siteType)
EnhancedFilterChip(
selected = selectedSiteFilter == siteType,
onClick = {
selectedSiteFilter = if (selectedSiteFilter == siteType) null else siteType
},
label = siteType.displayName,
color = color
)
}
// 즐겨찾기 필터
EnhancedFilterChip(
selected = showFavoritesOnly,
onClick = { showFavoritesOnly = !showFavoritesOnly },
label = "즐겨찾기",
color = MaterialTheme.colorScheme.error
)
}
}
}
@@ -210,14 +219,15 @@ fun DealListScreen(
}
is MainUiState.Success -> {
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter) {
state.deals.filter { deal ->
val matchesSearch = searchText.isBlank() ||
deal.title.contains(searchText, ignoreCase = true)
val matchesSite = selectedSiteFilter == null ||
deal.siteType == selectedSiteFilter
matchesSearch && matchesSite
}
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly) {
state.deals.filter { deal ->
val matchesSearch = searchText.isBlank() ||
deal.title.contains(searchText, ignoreCase = true)
val matchesSite = selectedSiteFilter == null ||
deal.siteType == selectedSiteFilter
val matchesFavorite = !showFavoritesOnly || deal.isFavorite
matchesSearch && matchesSite && matchesFavorite
}
}
if (filteredDeals.isEmpty()) {
@@ -289,13 +299,16 @@ fun DealListScreen(
),
exit = fadeOut(animationSpec = tween(200))
) {
DealItem(
deal = deal,
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
context.startActivity(intent)
}
)
DealItem(
deal = deal,
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
context.startActivity(intent)
},
onFavoriteToggle = { dealId ->
viewModel.toggleFavorite(dealId)
}
)
}
}

View File

@@ -104,11 +104,29 @@ class MainViewModel @Inject constructor(
}
}
fun toggleKeyword(id: Long, enabled: Boolean) {
viewModelScope.launch {
keywordDao.updateEnabled(id, enabled)
}
}
fun toggleKeyword(id: Long, enabled: Boolean) {
viewModelScope.launch {
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() {
workerScheduler.executeOnce()

View File

@@ -751,24 +751,21 @@ private fun EnhancedKeywordCard(
verticalAlignment = Alignment.CenterVertically
) {
// 키워드 아이콘
Box(
modifier = Modifier.size(40.dp)
.background(
if (keyword.isEnabled)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.Tag,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
Box(
modifier = Modifier.size(40.dp)
.background(
Color(0xFFFFCDD2), // 옅은 붉은색 (Light Red)
CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.Tag,
contentDescription = null,
tint = Color(0xFFD32F2F), // 붉은색 아이콘
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))