diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e19b8b2..c95c131 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "com.hotdeal.alarm" minSdk = 31 targetSdk = 35 - versionCode = 10 - versionName = "1.4.1" + versionCode = 11 + versionName = "1.5.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt index 5abf7fd..e652f1f 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt @@ -40,290 +40,321 @@ fun DealListScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - var searchText by remember { mutableStateOf("") } - var selectedSiteFilter by remember { mutableStateOf(null) } - var showFavoritesOnly by remember { mutableStateOf(false) } - var showFilterMenu by remember { mutableStateOf(false) } + var searchText by remember { mutableStateOf("") } + var selectedSiteFilter by remember { mutableStateOf(null) } + var showFavoritesOnly by remember { mutableStateOf(false) } + var showKeywordMatchOnly by remember { mutableStateOf(false) } + var showFilterMenu by remember { mutableStateOf(false) } - Column(modifier = Modifier.fillMaxSize()) { - TopAppBar( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(36.dp) - .background(MaterialTheme.colorScheme.primary, CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Filled.Notifications, - contentDescription = null, - 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 + Scaffold( + topBar = { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(36.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Notifications, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(20.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)) + Spacer(modifier = Modifier.width(12.dp)) Text( - text = "사이트 필터", - style = MaterialTheme.typography.titleSmall, + text = "핫딜 알람", + style = MaterialTheme.typography.titleLarge, 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 - ) - } - - // 즐겨찾기 필터 - 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) + }, + actions = { + BadgedBox( + badge = { + if (selectedSiteFilter != null || showFavoritesOnly || showKeywordMatchOnly) { + Badge( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary ) } } - } - - 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) - } - ) + 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 + ) + ) + }, + 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(16.dp)) + Spacer(modifier = Modifier.height(8.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( - message = state.message, - onRetry = { viewModel.refresh() } + // 검색창 + 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, 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() } + ) + } } } } diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt index 8225474..9d4b36a 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt @@ -124,13 +124,30 @@ class MainActivity : ComponentActivity() { } } - override fun onDestroy() { - super.onDestroy() - // 리시버 해제 - downloadReceiver?.let { - ApkDownloadManager.unregisterDownloadCompleteReceiver(this, it) - } - } + override fun onDestroy() { + super.onDestroy() + // 리시버 해제 + downloadReceiver?.let { + 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 + } /** * 업데이트 체크 diff --git a/app/src/main/java/com/hotdeal/alarm/ui/theme/Theme.kt b/app/src/main/java/com/hotdeal/alarm/ui/theme/Theme.kt index 80b0d18..0dbe31a 100644 --- a/app/src/main/java/com/hotdeal/alarm/ui/theme/Theme.kt +++ b/app/src/main/java/com/hotdeal/alarm/ui/theme/Theme.kt @@ -197,16 +197,19 @@ fun HotDealTheme( val view = LocalView.current if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = AppTypography, - content = content - ) + SideEffect { + val window = (view.context as Activity).window + // Edge-to-edge: 투명한 상태바 + 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, + content = content + ) +} } diff --git a/version.json b/version.json index 094f6c7..1704f35 100644 --- a/version.json +++ b/version.json @@ -1,13 +1,14 @@ { - "version": "1.4.1", - "versionCode": 10, + "version": "1.5.0", + "versionCode": 11, "minSdk": 31, "targetSdk": 35, "forceUpdate": false, "updateUrl": "https://git.webpluss.net/attachments/8571e491-bc80-4773-b6c7-32254adc4117", "changelog": [ - "설정 화면 진입 시 크래시 수정", - "AppDatabase 누락된 괄호 수정", - "REQUEST_INSTALL_PACKAGES 권한 예외 처리" + "Edge-to-edge UI 적용 (상태바/네비게이션바 투명)", + "내 키워드 필터 추가", + "백그라운드에서 포어그라운드 전환 시 자동 새로고침", + "WindowInsets 처리로 시스템 바 가림 해결" ] }