feat: UI/UX 개선 및 자동 새로고침 기능 추가
- Edge-to-edge UI 적용 (상태바/네비게이션바 투명) - WindowInsets 처리로 시스템 바 가림 해결 - 내 키워드 필터 추가 (키워드 매칭된 게시물만 표시) - 백그라운드에서 포어그라운드 전환 시 자동 새로고침 - Scaffold contentWindowInsets 적용 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -24,8 +24,8 @@ android {
|
|||||||
applicationId = "com.hotdeal.alarm"
|
applicationId = "com.hotdeal.alarm"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 10
|
versionCode = 11
|
||||||
versionName = "1.4.1"
|
versionName = "1.5.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -40,290 +40,321 @@ fun DealListScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
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 showFavoritesOnly by remember { mutableStateOf(false) }
|
||||||
var showFilterMenu by remember { mutableStateOf(false) }
|
var showKeywordMatchOnly by remember { mutableStateOf(false) }
|
||||||
|
var showFilterMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Scaffold(
|
||||||
TopAppBar(
|
topBar = {
|
||||||
title = {
|
TopAppBar(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
title = {
|
||||||
Box(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
modifier = Modifier
|
Box(
|
||||||
.size(36.dp)
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.primary, CircleShape),
|
.size(36.dp)
|
||||||
contentAlignment = Alignment.Center
|
.background(MaterialTheme.colorScheme.primary, CircleShape),
|
||||||
) {
|
contentAlignment = Alignment.Center
|
||||||
Icon(
|
) {
|
||||||
imageVector = Icons.Filled.Notifications,
|
Icon(
|
||||||
contentDescription = null,
|
imageVector = Icons.Filled.Notifications,
|
||||||
tint = MaterialTheme.colorScheme.onPrimary,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp)
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
)
|
modifier = Modifier.size(20.dp)
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "핫딜 알람",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
BadgedBox(
|
|
||||||
badge = {
|
|
||||||
if (selectedSiteFilter != null) {
|
|
||||||
Badge(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
) {
|
|
||||||
IconButton(onClick = { showFilterMenu = !showFilterMenu }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (showFilterMenu) Icons.Filled.FilterList else Icons.Outlined.FilterList,
|
|
||||||
contentDescription = "필터"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(onClick = { viewModel.refresh() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Refresh,
|
|
||||||
contentDescription = "새로고침"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { onNavigateToSettings() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Settings,
|
|
||||||
contentDescription = "설정"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = showFilterMenu,
|
|
||||||
enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeIn(),
|
|
||||||
exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeOut()
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.FilterAlt,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = "사이트 필터",
|
text = "핫딜 알람",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
actions = {
|
||||||
|
BadgedBox(
|
||||||
Row(
|
badge = {
|
||||||
modifier = Modifier
|
if (selectedSiteFilter != null || showFavoritesOnly || showKeywordMatchOnly) {
|
||||||
.fillMaxWidth()
|
Badge(
|
||||||
.horizontalScroll(rememberScrollState()),
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
|
||||||
EnhancedFilterChip(
|
|
||||||
selected = selectedSiteFilter == null,
|
|
||||||
onClick = { selectedSiteFilter = null },
|
|
||||||
label = "전체",
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 즐겨찾기 필터
|
|
||||||
EnhancedFilterChip(
|
|
||||||
selected = showFavoritesOnly,
|
|
||||||
onClick = { showFavoritesOnly = !showFavoritesOnly },
|
|
||||||
label = "즐겨찾기",
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = searchText,
|
|
||||||
onValueChange = { searchText = it },
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = "제목으로 검색...",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
singleLine = true,
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Search,
|
|
||||||
contentDescription = "검색",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
if (searchText.isNotEmpty()) {
|
|
||||||
IconButton(onClick = { searchText = "" }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Close,
|
|
||||||
contentDescription = "지우기",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
|
||||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
when (val state = uiState) {
|
|
||||||
is MainUiState.Loading -> {
|
|
||||||
DealListSkeleton(count = 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
is MainUiState.Success -> {
|
|
||||||
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()) {
|
|
||||||
if (state.deals.isEmpty()) {
|
|
||||||
NoDealsState(onRefresh = { viewModel.refresh() })
|
|
||||||
} else {
|
|
||||||
val selectedFilter = selectedSiteFilter
|
|
||||||
val message = when {
|
|
||||||
selectedFilter != null -> "${selectedFilter.displayName}의 핫딜이 없습니다"
|
|
||||||
searchText.isNotBlank() -> "'$searchText'에 대한 검색 결과가 없습니다"
|
|
||||||
else -> "표시할 핫딜이 없습니다"
|
|
||||||
}
|
|
||||||
EmptyState(
|
|
||||||
title = "결과가 없습니다",
|
|
||||||
message = message,
|
|
||||||
icon = Icons.Outlined.Search
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Inventory2,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(
|
|
||||||
text = "${filteredDeals.size}개의 핫딜",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
if (selectedSiteFilter != null) {
|
|
||||||
val filter = selectedSiteFilter!!
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "• ${filter.displayName}",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = getSiteColor(filter)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
items(
|
IconButton(onClick = { showFilterMenu = !showFilterMenu }) {
|
||||||
items = filteredDeals,
|
Icon(
|
||||||
key = { it.id }
|
imageVector = if (showFilterMenu) Icons.Filled.FilterList else Icons.Outlined.FilterList,
|
||||||
) { deal ->
|
contentDescription = "필터"
|
||||||
AnimatedVisibility(
|
)
|
||||||
visible = true,
|
}
|
||||||
enter = fadeIn(animationSpec = tween(300)) +
|
}
|
||||||
slideInVertically(
|
IconButton(onClick = { viewModel.refresh() }) {
|
||||||
animationSpec = tween(300),
|
Icon(
|
||||||
initialOffsetY = { it / 8 }
|
imageVector = Icons.Outlined.Refresh,
|
||||||
),
|
contentDescription = "새로고침"
|
||||||
exit = fadeOut(animationSpec = tween(200))
|
)
|
||||||
) {
|
}
|
||||||
DealItem(
|
IconButton(onClick = { onNavigateToSettings() }) {
|
||||||
deal = deal,
|
Icon(
|
||||||
onClick = {
|
imageVector = Icons.Outlined.Settings,
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
|
contentDescription = "설정"
|
||||||
context.startActivity(intent)
|
)
|
||||||
},
|
}
|
||||||
onFavoriteToggle = { dealId ->
|
},
|
||||||
viewModel.toggleFavorite(dealId)
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
}
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.systemBars
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
// 필터 메뉴
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showFilterMenu,
|
||||||
|
enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeIn(),
|
||||||
|
exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeOut()
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.FilterAlt,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "사이트 필터",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
EnhancedFilterChip(
|
||||||
|
selected = selectedSiteFilter == null,
|
||||||
|
onClick = { selectedSiteFilter = null },
|
||||||
|
label = "전체",
|
||||||
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
// 특수 필터
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 내 키워드 필터
|
||||||
|
EnhancedFilterChip(
|
||||||
|
selected = showKeywordMatchOnly,
|
||||||
|
onClick = { showKeywordMatchOnly = !showKeywordMatchOnly },
|
||||||
|
label = "내 키워드",
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
// 즐겨찾기 필터
|
||||||
|
EnhancedFilterChip(
|
||||||
|
selected = showFavoritesOnly,
|
||||||
|
onClick = { showFavoritesOnly = !showFavoritesOnly },
|
||||||
|
label = "즐겨찾기",
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MainUiState.Error -> {
|
// 검색창
|
||||||
ErrorState(
|
OutlinedTextField(
|
||||||
message = state.message,
|
value = searchText,
|
||||||
onRetry = { viewModel.refresh() }
|
onValueChange = { searchText = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "제목으로 검색...",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Search,
|
||||||
|
contentDescription = "검색",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (searchText.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { searchText = "" }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = "지우기",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 딜 리스트
|
||||||
|
when (val state = uiState) {
|
||||||
|
is MainUiState.Loading -> {
|
||||||
|
DealListSkeleton(count = 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainUiState.Success -> {
|
||||||
|
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly, showKeywordMatchOnly) {
|
||||||
|
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
|
||||||
|
val matchesKeyword = !showKeywordMatchOnly || deal.isKeywordMatch
|
||||||
|
matchesSearch && matchesSite && matchesFavorite && matchesKeyword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredDeals.isEmpty()) {
|
||||||
|
if (state.deals.isEmpty()) {
|
||||||
|
NoDealsState(onRefresh = { viewModel.refresh() })
|
||||||
|
} else {
|
||||||
|
val selectedFilter = selectedSiteFilter
|
||||||
|
val message = when {
|
||||||
|
showKeywordMatchOnly -> "키워드 매칭된 핫딜이 없습니다"
|
||||||
|
showFavoritesOnly -> "즐겨찾기한 핫딜이 없습니다"
|
||||||
|
selectedFilter != null -> "${selectedFilter.displayName}의 핫딜이 없습니다"
|
||||||
|
searchText.isNotBlank() -> "'$searchText'에 대한 검색 결과가 없습니다"
|
||||||
|
else -> "표시할 핫딜이 없습니다"
|
||||||
|
}
|
||||||
|
EmptyState(
|
||||||
|
title = "결과가 없습니다",
|
||||||
|
message = message,
|
||||||
|
icon = Icons.Outlined.Search
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Inventory2,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "${filteredDeals.size}개의 핫딜",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
if (selectedSiteFilter != null) {
|
||||||
|
val filter = selectedSiteFilter!!
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "• ${filter.displayName}",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = getSiteColor(filter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = filteredDeals,
|
||||||
|
key = { it.id }
|
||||||
|
) { deal ->
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = true,
|
||||||
|
enter = fadeIn(animationSpec = tween(300)) +
|
||||||
|
slideInVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
initialOffsetY = { it / 8 }
|
||||||
|
),
|
||||||
|
exit = fadeOut(animationSpec = tween(200))
|
||||||
|
) {
|
||||||
|
DealItem(
|
||||||
|
deal = deal,
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
|
||||||
|
context.startActivity(intent)
|
||||||
|
},
|
||||||
|
onFavoriteToggle = { dealId ->
|
||||||
|
viewModel.toggleFavorite(dealId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainUiState.Error -> {
|
||||||
|
ErrorState(
|
||||||
|
message = state.message,
|
||||||
|
onRetry = { viewModel.refresh() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,13 +124,30 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// 리시버 해제
|
// 리시버 해제
|
||||||
downloadReceiver?.let {
|
downloadReceiver?.let {
|
||||||
ApkDownloadManager.unregisterDownloadCompleteReceiver(this, it)
|
ApkDownloadManager.unregisterDownloadCompleteReceiver(this, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 비저빌리티 콜백 - 백그라운드에서 포어그라운드 전환 시 새로고침
|
||||||
|
private var isInBackground = false
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (isInBackground) {
|
||||||
|
// 백그라운드에서 돌아왔을 때 새로고침
|
||||||
|
workerScheduler.executeOnce()
|
||||||
|
isInBackground = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
isInBackground = true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 업데이트 체크
|
* 업데이트 체크
|
||||||
|
|||||||
@@ -197,16 +197,19 @@ fun HotDealTheme(
|
|||||||
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
window.statusBarColor = colorScheme.primary.toArgb()
|
// Edge-to-edge: 투명한 상태바
|
||||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||||
}
|
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||||
}
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||||
|
WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme
|
||||||
MaterialTheme(
|
}
|
||||||
colorScheme = colorScheme,
|
|
||||||
typography = AppTypography,
|
MaterialTheme(
|
||||||
content = content
|
colorScheme = colorScheme,
|
||||||
)
|
typography = AppTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
version.json
11
version.json
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"versionCode": 10,
|
"versionCode": 11,
|
||||||
"minSdk": 31,
|
"minSdk": 31,
|
||||||
"targetSdk": 35,
|
"targetSdk": 35,
|
||||||
"forceUpdate": false,
|
"forceUpdate": false,
|
||||||
"updateUrl": "https://git.webpluss.net/attachments/8571e491-bc80-4773-b6c7-32254adc4117",
|
"updateUrl": "https://git.webpluss.net/attachments/8571e491-bc80-4773-b6c7-32254adc4117",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"설정 화면 진입 시 크래시 수정",
|
"Edge-to-edge UI 적용 (상태바/네비게이션바 투명)",
|
||||||
"AppDatabase 누락된 괄호 수정",
|
"내 키워드 필터 추가",
|
||||||
"REQUEST_INSTALL_PACKAGES 권한 예외 처리"
|
"백그라운드에서 포어그라운드 전환 시 자동 새로고침",
|
||||||
|
"WindowInsets 처리로 시스템 바 가림 해결"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user