feat: UI/UX 대폭 개선 및 최적화 (v1.1.0)

- 알림 권한 설정 UI 상세화 (아이콘, 설명 추가)
- 폴링 주기 오타 수정 (폴� -> 폴링)
- 키워드 매칭 시각화 강화 (별 아이콘, 강한 테두리)
- 하단 네비게이션 제거, 상단 앱바로 설정 이동
- 배터리/데이터 최적화 (데이터 보관 기간 7일 -> 3일)
- 버전 업데이트 (1.0.1 -> 1.1.0)
This commit is contained in:
sanjeok77
2026-03-04 02:49:32 +09:00
parent c67bb57be7
commit 745dd1a174
7 changed files with 185 additions and 142 deletions

View File

@@ -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 {

View File

@@ -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.*
@@ -47,23 +47,22 @@ fun DealItem(
val siteColor = getSiteColor(deal.siteType)
// 키워드 매칭된 핫딜은 다른 색상으로 강조
// 키워드 매칭된 핫딜은 더 강한 시각적 강조
val cardColor = if (deal.isKeywordMatch) {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
} else {
MaterialTheme.colorScheme.surface
}
val borderColor = if (deal.isKeywordMatch) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
MaterialTheme.colorScheme.primary
} else {
Color.Transparent
}
val cardModifier = if (deal.isKeywordMatch) {
modifier
.fillMaxWidth()
.border(2.dp, borderColor, MaterialTheme.shapes.large)
modifier.fillMaxWidth()
.border(3.dp, borderColor, MaterialTheme.shapes.large)
} else {
modifier.fillMaxWidth()
}
@@ -123,7 +122,7 @@ fun DealItem(
Spacer(modifier = Modifier.weight(1f))
// 키워드 매칭 배지 (있을 때만)
// 키워드 매칭 배지 - 별 아이콘으로 변경
if (deal.isKeywordMatch) {
Surface(
shape = RoundedCornerShape(12.dp),
@@ -136,14 +135,15 @@ fun DealItem(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Icon(
imageVector = Icons.Default.NotificationsActive,
imageVector = Icons.Default.Star,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(14.dp)
)
Text(
text = "키워드 매칭",
text = "키워드",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimary
)
}

View File

@@ -57,28 +57,22 @@ 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) }
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(
@@ -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)
}

View File

@@ -174,19 +174,64 @@ fun PermissionCard(
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) {
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.Check else Icons.Default.Close,
imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.CheckCircle else Icons.Default.Warning,
contentDescription = null,
tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text("알림 권한", modifier = Modifier.weight(1f))
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) {
Button(onClick = onRequestPermission) { Text("설정") }
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,
@@ -194,7 +239,14 @@ fun PermissionCard(
tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.width(12.dp))
Text("사이트 선택")
Column {
Text("사이트 선택", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium)
Text(
text = if (hasEnabledSites) "모니터링할 사이트가 선택되었습니다" else "최소 1개 사이트를 선택해주세요",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@@ -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("<EFBFBD> 주기", 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 ->

View File

@@ -116,8 +116,8 @@ class HotDealPollingWorker @AssistedInject constructor(
// 8. 알림 발송 상태 업데이트
dealDao.markAsNotified(newDeals.map { it.id })
// 9. 오래된 데이터 정리 (7일 이상)
val threshold = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L)
// 9. 오래된 데이터 정리 (3일 이상 - 배터리/데이터 절약)
val threshold = System.currentTimeMillis() - (3 * 24 * 60 * 60 * 1000L)
dealDao.deleteOldDeals(threshold)
Log.d(TAG, "===== 핫딜 폴<> 완료 =====")

View File

@@ -34,6 +34,7 @@ class WorkerScheduler @Inject constructor(
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true) // 배터리 부족 시 실행 안 함
.setRequiresDeviceIdle(false) // 화면 켜진 상태에서도 실행
.build()
// 유연한 실행 시간 (±25%)

View File

@@ -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)",
"버그 수정"
"UI/UX 대폭 개선",
"알림 권한 설정 상세화",
"키워드 매칭 시각화 강화 (별 아이콘)",
"하단 메뉴 제거, 상단 앱바로 이동",
"배터리/데이터 최적화",
"폴링 주기 오타 수정"
]
}
}