From 745dd1a174811442a03ab39a4f2c2a766aa77d3f Mon Sep 17 00:00:00 2001 From: sanjeok77 Date: Wed, 4 Mar 2026 02:49:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20UI/UX=20=EB=8C=80=ED=8F=AD=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EC=B5=9C=EC=A0=81=ED=99=94=20(v1.1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 권한 설정 UI 상세화 (아이콘, 설명 추가) - 폴링 주기 오타 수정 (폴� -> 폴링) - 키워드 매칭 시각화 강화 (별 아이콘, 강한 테두리) - 하단 네비게이션 제거, 상단 앱바로 설정 이동 - 배터리/데이터 최적화 (데이터 보관 기간 7일 -> 3일) - 버전 업데이트 (1.0.1 -> 1.1.0) --- app/build.gradle.kts | 4 +- .../alarm/presentation/components/DealItem.kt | 98 +++++++-------- .../alarm/presentation/main/MainScreen.kt | 79 +++++------- .../presentation/settings/SettingsScreen.kt | 114 +++++++++++++----- .../alarm/worker/HotDealPollingWorker.kt | 6 +- .../hotdeal/alarm/worker/WorkerScheduler.kt | 9 +- version.json | 17 +-- 7 files changed, 185 insertions(+), 142 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ca83e6..80e427c 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 = 2 - versionName = "1.0.1" + versionCode = 3 + versionName = "1.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt index 461a70d..0ceb514 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Share import androidx.compose.material3.* import androidx.compose.runtime.* @@ -45,28 +45,27 @@ fun DealItem( label = "favorite_scale" ) - val siteColor = getSiteColor(deal.siteType) - - // 키워드 매칭된 핫딜은 다른 색상으로 강조 - val cardColor = if (deal.isKeywordMatch) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } else { - MaterialTheme.colorScheme.surface - } - - val borderColor = if (deal.isKeywordMatch) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) - } else { - Color.Transparent - } - - val cardModifier = if (deal.isKeywordMatch) { - modifier - .fillMaxWidth() - .border(2.dp, borderColor, MaterialTheme.shapes.large) - } else { + val siteColor = getSiteColor(deal.siteType) + + // 키워드 매칭된 핫딜은 더 강한 시각적 강조 + val cardColor = if (deal.isKeywordMatch) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + } else { + MaterialTheme.colorScheme.surface + } + + val borderColor = if (deal.isKeywordMatch) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + } + + val cardModifier = if (deal.isKeywordMatch) { modifier.fillMaxWidth() - } + .border(3.dp, borderColor, MaterialTheme.shapes.large) + } else { + modifier.fillMaxWidth() + } ElevatedCard( modifier = cardModifier, @@ -123,33 +122,34 @@ fun DealItem( Spacer(modifier = Modifier.weight(1f)) - // 키워드 매칭 배지 (있을 때만) - if (deal.isKeywordMatch) { - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.height(26.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Icon( - imageVector = Icons.Default.NotificationsActive, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(14.dp) - ) - Text( - text = "키워드 매칭", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimary - ) - } - } - Spacer(modifier = Modifier.width(Spacing.xs)) - } + // 키워드 매칭 배지 - 별 아이콘으로 변경 + if (deal.isKeywordMatch) { + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.height(26.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(14.dp) + ) + Text( + text = "내 키워드", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + Spacer(modifier = Modifier.width(Spacing.xs)) + } // 공유 버튼 IconButton( diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt index 5145f5d..fd13d3c 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt @@ -56,44 +56,38 @@ fun MainScreen( } } - Scaffold( - bottomBar = { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - - BottomNavItem.values().forEach { item -> - NavigationBarItem( - selected = currentRoute == item.route, - onClick = { - navController.navigate(item.route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { Icon(item.icon, contentDescription = item.label) }, - label = { Text(item.label) } - ) - } - } - } - ) { padding -> - NavHost( - navController = navController, - startDestination = Screen.DealList.route, - modifier = Modifier.padding(padding) - ) { - composable(Screen.DealList.route) { - DealListScreen(viewModel = viewModel) - } - composable(Screen.Settings.route) { - SettingsScreen(viewModel = viewModel) - } - } - } + Scaffold( + topBar = { + TopAppBar( + title = { Text("핫딜 알람") }, + actions = { + // 설정 버튼 + IconButton(onClick = { navController.navigate(Screen.Settings.route) }) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "설정" + ) + } + }, + colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + ) { padding -> + NavHost( + navController = navController, + startDestination = Screen.DealList.route, + modifier = Modifier.padding(padding) + ) { + composable(Screen.DealList.route) { + DealListScreen(viewModel = viewModel) + } + composable(Screen.Settings.route) { + SettingsScreen(viewModel = viewModel) + } + } + } if (showPermissionDialog) { PermissionDialog( @@ -110,11 +104,4 @@ sealed class Screen(val route: String) { object Settings : Screen("settings") } -enum class BottomNavItem( - val route: String, - val label: String, - val icon: androidx.compose.ui.graphics.vector.ImageVector -) { - Deals("deal_list", "핫딜", Icons.Default.List), - Settings("settings", "설정", Icons.Default.Settings) -} + diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt index 09f776d..d14cfa6 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt @@ -167,37 +167,89 @@ fun SettingsScreen(viewModel: MainViewModel) { @Composable fun PermissionCard( - permissionStatus: PermissionHelper.PermissionStatus, - hasEnabledSites: Boolean, - onRequestPermission: () -> Unit + permissionStatus: PermissionHelper.PermissionStatus, + hasEnabledSites: Boolean, + onRequestPermission: () -> Unit ) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("알림 권한", modifier = Modifier.weight(1f)) - if (!permissionStatus.hasNotificationPermission) { - Button(onClick = onRequestPermission) { Text("설정") } - } - } - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("사이트 선택") - } - } - } + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(12.dp)) + + // 알림 권한 상세 안내 + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium, + color = if (permissionStatus.hasNotificationPermission) + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + else + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.CheckCircle else Icons.Default.Warning, + contentDescription = null, + tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (permissionStatus.hasNotificationPermission) + "키워드 매칭 시 즉시 알림을 받을 수 있습니다" + else + "키워드 매칭 알림을 받으려면 권한을 허용해주세요", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + if (!permissionStatus.hasNotificationPermission) { + Spacer(modifier = Modifier.height(12.dp)) + Button( + onClick = onRequestPermission, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.Notifications, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("알림 권한 허용하기") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(16.dp)) + + // 사이트 선택 상태 + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text("사이트 선택", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) + Text( + text = if (hasEnabledSites) "모니터링할 사이트가 선택되었습니다" else "최소 1개 사이트를 선택해주세요", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } } @Composable @@ -205,7 +257,7 @@ fun PollingIntervalCard(currentInterval: Int, onIntervalChange: (Long) -> Unit) var selected by remember(currentInterval) { mutableStateOf(currentInterval) } Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { - Text("폴� 주기", style = MaterialTheme.typography.titleMedium) + Text("폴링 주기", style = MaterialTheme.typography.titleMedium) Text("2분 권장", style = MaterialTheme.typography.bodySmall) Spacer(modifier = Modifier.height(8.dp)) listOf(1, 2, 5, 10, 15, 30).forEach { minutes -> diff --git a/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt b/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt index 170571e..8815771 100644 --- a/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt +++ b/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt @@ -116,9 +116,9 @@ class HotDealPollingWorker @AssistedInject constructor( // 8. 알림 발송 상태 업데이트 dealDao.markAsNotified(newDeals.map { it.id }) - // 9. 오래된 데이터 정리 (7일 이상) - val threshold = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L) - dealDao.deleteOldDeals(threshold) + // 9. 오래된 데이터 정리 (3일 이상 - 배터리/데이터 절약) + val threshold = System.currentTimeMillis() - (3 * 24 * 60 * 60 * 1000L) + dealDao.deleteOldDeals(threshold) Log.d(TAG, "===== 핫딜 폴� 완료 =====") Result.success() diff --git a/app/src/main/java/com/hotdeal/alarm/worker/WorkerScheduler.kt b/app/src/main/java/com/hotdeal/alarm/worker/WorkerScheduler.kt index d246398..3a9a957 100644 --- a/app/src/main/java/com/hotdeal/alarm/worker/WorkerScheduler.kt +++ b/app/src/main/java/com/hotdeal/alarm/worker/WorkerScheduler.kt @@ -31,10 +31,11 @@ class WorkerScheduler @Inject constructor( // 최소/최대 값 제한 val safeInterval = intervalMinutes.coerceIn(MIN_INTERVAL_MINUTES, MAX_INTERVAL_MINUTES) - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) // 배터리 부족 시 실행 안 함 - .build() + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) // 배터리 부족 시 실행 안 함 + .setRequiresDeviceIdle(false) // 화면 켜진 상태에서도 실행 + .build() // 유연한 실행 시간 (±25%) val flexTime = (safeInterval * 0.25).toLong().coerceAtLeast(1) diff --git a/version.json b/version.json index 17b26d1..5194eda 100644 --- a/version.json +++ b/version.json @@ -1,14 +1,17 @@ { - "version": "1.0.1", - "versionCode": 2, + "version": "1.1.0", + "versionCode": 3, "minSdk": 31, "targetSdk": 35, "forceUpdate": false, "updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases", - "changelog": [ - "폴링 주기 설정 수정", - "릴리즈 빌드 (서명된 APK)", - "버그 수정" - ] + "changelog": [ + "UI/UX 대폭 개선", + "알림 권한 설정 상세화", + "키워드 매칭 시각화 강화 (별 아이콘)", + "하단 메뉴 제거, 상단 앱바로 이동", + "배터리/데이터 최적화", + "폴링 주기 오타 수정" + ] } }