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 02c118f..b8c509f 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 @@ -51,10 +51,10 @@ fun DealItem( val siteColor = getSiteColor(deal.siteType) - // 키워드 매칭 배경 - 그라데이션 효과 + // 키워드 매칭 배경 - 옅은 붉은색 단색 배경 val cardColors = if (deal.isKeywordMatch) { CardDefaults.elevatedCardColors( - containerColor = Color.Transparent + containerColor = Color(0xFFFCE8E8) // 옅은 붉은색 ) } else { CardDefaults.elevatedCardColors( @@ -63,38 +63,14 @@ fun DealItem( } Box(modifier = modifier) { - if (deal.isKeywordMatch) { - // 키워드 매칭: 그라데이션 배경 - Box( - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .clip(RoundedCornerShape(20.dp)) - .background( - Brush.linearGradient( - colors = listOf( - Color(0xFFFFEBEE), - Color(0xFFFFCDD2).copy(alpha = 0.4f) - ) - ) - ) - ) - } + // 키워드 매칭 게시물도 일반 카드와 동일한 형태로 표시 (색상만 다름) ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .then( - if (deal.isKeywordMatch) { - Modifier.background(Color.Transparent) - } else { - Modifier - } - ), + modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), colors = cardColors, elevation = CardDefaults.elevatedCardElevation( - defaultElevation = if (deal.isKeywordMatch) 0.dp else 2.dp + defaultElevation = 2.dp ), onClick = onClick ) { @@ -299,4 +275,4 @@ private fun formatTime(timestamp: Long): String { diff < 86_400_000 -> "${diff / 3_600_000}시간 전" else -> "${diff / 86_400_000}일 전" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt index 03fca6e..7cf6183 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt @@ -21,10 +21,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.hotdeal.alarm.ui.theme.Spacing /** - * 빈 상태 컴포넌트 - 개선된 디자인 + * 빈 상태 컴포넌트 - 프리미엄 디자인 */ @Composable fun EmptyState( @@ -37,56 +36,76 @@ fun EmptyState( Column( modifier = modifier .fillMaxSize() - .padding(Spacing.lg), + .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // 아이콘 배경 + // 그라데이션 배경 아이콘 Box( modifier = Modifier - .size(120.dp) + .size(140.dp) .background( - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), - CircleShape + brush = Brush.radialGradient( + colors = listOf( + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f), + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ) + ), + shape = CircleShape ), contentAlignment = Alignment.Center ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - ) + Box( + modifier = Modifier + .size(100.dp) + .background( + brush = Brush.linearGradient( + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.tertiary + ) + ), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(56.dp), + tint = Color.White + ) + } } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(28.dp)) Text( text = title, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(10.dp)) Text( text = message, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center ) if (action != null) { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(28.dp)) action() } } } /** - * 핫딜 없음 상태 - 개선된 디자인 + * 핫딜 없음 상태 - 프리미엄 디자인 */ @Composable fun NoDealsState( @@ -95,25 +114,28 @@ fun NoDealsState( ) { EmptyState( title = "수집된 핫딜이 없습니다", - message = "새로고침하여 최신 핫딜을 확인해보세요", + message = "새로고침하여 최신 핫딜을 확인핳보세요", icon = Icons.Outlined.ShoppingBag, modifier = modifier, action = { - FilledTonalButton( + Button( onClick = onRefresh, shape = RoundedCornerShape(16.dp), - modifier = Modifier.height(48.dp) + modifier = Modifier.height(52.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) ) { Icon( imageVector = Icons.Filled.Refresh, contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(22.dp) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(10.dp)) Text( text = "새로고침", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold ) } } @@ -137,7 +159,7 @@ fun NoSearchResultState( } /** - * 에러 상태 - 개선된 디자인 + * 에러 상태 - 프리미엄 디자인 */ @Composable fun ErrorState( @@ -145,39 +167,92 @@ fun ErrorState( onRetry: () -> Unit, modifier: Modifier = Modifier ) { - EmptyState( - title = "오류가 발생했습니다", - message = message, - icon = Icons.Outlined.ErrorOutline, - modifier = modifier, - action = { - FilledTonalButton( - onClick = onRetry, - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer + Column( + modifier = modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // 에러 아이콘 배경 + Box( + modifier = Modifier + .size(140.dp) + .background( + brush = Brush.radialGradient( + colors = listOf( + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f), + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) + ) + ), + shape = CircleShape ), - modifier = Modifier.height(48.dp) + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(100.dp) + .background( + color = MaterialTheme.colorScheme.error, + shape = CircleShape + ), + contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Filled.Refresh, + imageVector = Icons.Outlined.ErrorOutline, contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "다시 시도", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium + modifier = Modifier.size(56.dp), + tint = Color.White ) } } - ) + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = "오류가 발생했습니다", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(28.dp)) + + Button( + onClick = onRetry, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.height(52.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = "다시 시도", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + } + } } /** - * 로딩 스켈레톤 - 개선된 디자인 + * 로딩 스켈레톤 - 프리미엄 디자인 */ @Composable fun DealListSkeleton( @@ -209,6 +284,18 @@ private fun DealItemSkeleton( visible = true } + // Shimmer 애니메이션 + val shimmerAnim = rememberInfiniteTransition(label = "shimmer") + val shimmerTranslate by shimmerAnim.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "shimmer_translate" + ) + AnimatedVisibility( visible = visible && isVisible, enter = fadeIn(animationSpec = tween(300)) + slideInVertically( @@ -220,7 +307,7 @@ private fun DealItemSkeleton( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) ), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) ) { @@ -233,82 +320,95 @@ private fun DealItemSkeleton( verticalAlignment = Alignment.CenterVertically ) { // 사이트 뱃지 스켈레톤 - Box( + SkeletonBox( modifier = Modifier - .height(28.dp) - .width(80.dp) - .clip(RoundedCornerShape(10.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) + .height(32.dp) + .width(90.dp) + .clip(RoundedCornerShape(12.dp)) ) Spacer(modifier = Modifier.width(8.dp)) // 게시판 스켈레톤 - Box( + SkeletonBox( modifier = Modifier - .height(24.dp) - .width(60.dp) + .height(28.dp) + .width(70.dp) .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) ) Spacer(modifier = Modifier.weight(1f)) // 액션 버튼 스켈레톤 - Box( + SkeletonBox( modifier = Modifier .size(36.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant) ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(14.dp)) // 제목 스켈레톤 - Box( + SkeletonBox( modifier = Modifier - .fillMaxWidth(0.9f) - .height(20.dp) - .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxWidth(0.92f) + .height(22.dp) + .clip(RoundedCornerShape(6.dp)) ) Spacer(modifier = Modifier.height(8.dp)) - Box( + SkeletonBox( modifier = Modifier - .fillMaxWidth(0.6f) - .height(20.dp) - .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxWidth(0.65f) + .height(22.dp) + .clip(RoundedCornerShape(6.dp)) ) - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(12.dp)) // 하단 스켈레톤 Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Box( + SkeletonBox( modifier = Modifier - .height(14.dp) - .width(60.dp) + .height(16.dp) + .width(70.dp) .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) ) Spacer(modifier = Modifier.weight(1f)) - Box( + SkeletonBox( modifier = Modifier - .size(16.dp) + .size(32.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant) ) } } } } } + +@Composable +private fun SkeletonBox(modifier: Modifier = Modifier) { + val shimmerAnim = rememberInfiniteTransition(label = "skeleton_shimmer") + val shimmerAlpha by shimmerAnim.animateFloat( + initialValue = 0.3f, + targetValue = 0.7f, + animationSpec = infiniteRepeatable( + animation = tween(800, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "shimmer_alpha" + ) + + Box( + modifier = modifier.background( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = shimmerAlpha) + ) + ) +} \ No newline at end of file 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 c355b97..ac3bffd 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 @@ -22,19 +22,20 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.compose.ui.draw.scale import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hotdeal.alarm.domain.model.SiteType import com.hotdeal.alarm.presentation.components.* import com.hotdeal.alarm.presentation.main.MainUiState import com.hotdeal.alarm.presentation.main.MainViewModel import com.hotdeal.alarm.ui.theme.getSiteColor +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -73,6 +74,14 @@ fun DealListScreen( var showKeywordMatchOnly by remember { mutableStateOf(false) } var showFilterMenu by remember { mutableStateOf(false) } + // 필터 메뉴 자동 닫기 (5초) + LaunchedEffect(showFilterMenu) { + if (showFilterMenu) { + delay(5000L) + showFilterMenu = false + } + } + Scaffold( topBar = { TopAppBar( @@ -130,13 +139,13 @@ fun DealListScreen( ) } }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - scrolledContainerColor = MaterialTheme.colorScheme.surface - ), - modifier = Modifier.statusBarsPadding() - ) - }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface + ), + windowInsets = WindowInsets(0, 0, 0, 0) + ) + }, floatingActionButton = { AnimatedVisibility( visible = showScrollToTop, @@ -150,7 +159,7 @@ fun DealListScreen( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, shape = CircleShape, - modifier = Modifier.navigationBarsPadding() + modifier = Modifier.navigationBarsPadding() ) { Icon( imageVector = Icons.Filled.KeyboardArrowUp, @@ -398,10 +407,10 @@ fun DealListScreen( } } - // EdgeToEdge: 하단 네비게이션 바 공간 확보 - item { - Spacer(modifier = Modifier.height(16.dp)) - } + // EdgeToEdge: 하단 네비게이션 바 공간 확보 + item { + Spacer(modifier = Modifier.height(16.dp)) + } } } } @@ -414,17 +423,22 @@ fun DealListScreen( } } } - - // Pull to Refresh 인디케이터 - 상단 패딩으로 잘 보이게 - PullToRefreshContainer( - state = pullToRefreshState, - modifier = Modifier - .align(Alignment.TopCenter) - .padding(top = 8.dp) // TopAppBar와 겹치지 않도록 패딩 - .zIndex(999f), - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) + + // Pull to Refresh 인디케이터 - 절대 위치로 배치하여 간격 벌어짐 방지 + val progress = pullToRefreshState.progress + val showIndicator = pullToRefreshState.isRefreshing || progress > 0 + + if (showIndicator) { + PullToRefreshContainer( + state = pullToRefreshState, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 100.dp) + .zIndex(999f), + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + } // 새로고침 트리거 LaunchedEffect(pullToRefreshState.isRefreshing) { @@ -480,4 +494,4 @@ private fun EnhancedFilterChip( ) } } -} +} \ No newline at end of file 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 471b028..685537b 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 @@ -137,6 +137,22 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) // One UI 7+에서 시스템 바 인셋 처리 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + } + // One UI 7+에서 시스템 바 인셋 처리 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + // 상단 패딩은 제거 (Compose에서 직접 처리) + view.setPadding(systemBars.left, 0, systemBars.right, systemBars.bottom) + insets + } + } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt index 32faf19..cba5f56 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt @@ -12,6 +12,8 @@ import com.hotdeal.alarm.worker.WorkerScheduler import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject @HiltViewModel @@ -145,6 +147,17 @@ class MainViewModel @Inject constructor( fun stopPolling() { workerScheduler.cancelPolling() } + + // 데이터 파싱 핫딜 데이터 전체 삭제 및 사용자 피드백 트리거 + private val _toastEvent = MutableSharedFlow(extraBufferCapacity = 1) + val toastEvent = _toastEvent.asSharedFlow() + + fun deleteAllParsedData() { + viewModelScope.launch { + hotDealDao.deleteAllDeals() + _toastEvent.emit("파싱 데이터가 삭제되었습니다") + } + } } sealed class MainUiState { diff --git a/app/src/main/java/com/hotdeal/alarm/service/NotificationService.kt b/app/src/main/java/com/hotdeal/alarm/service/NotificationService.kt index 5124909..8bb847e 100644 --- a/app/src/main/java/com/hotdeal/alarm/service/NotificationService.kt +++ b/app/src/main/java/com/hotdeal/alarm/service/NotificationService.kt @@ -7,6 +7,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -110,8 +111,9 @@ class NotificationService @Inject constructor( .setContentText(deals.first().title) .setSmallIcon(android.R.drawable.ic_menu_send) .setContentIntent(pendingIntent) - .setAutoCancel(true) - .build() + .setAutoCancel(true) + .setNumber(deals.size) // 뱃지 숫자 설정 + .build() } else { NotificationCompat.Builder(context, CHANNEL_NORMAL) .setContentTitle(context.getString(R.string.notification_new_deal)) @@ -134,29 +136,55 @@ class NotificationService @Inject constructor( */ fun showKeywordMatchNotification(deals: List) { if (!hasNotificationPermission()) return - - val intent = Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - val pendingIntent = PendingIntent.getActivity( - context, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val notification = NotificationCompat.Builder(context, CHANNEL_URGENT) - .setContentTitle(context.getString(R.string.notification_keyword_match)) - .setContentText(deals.first().title) - .setSmallIcon(android.R.drawable.ic_menu_send) - .setContentIntent(pendingIntent) + + if (deals.size == 1) { + // 단일 딜인 경우 해당 URL로 바로 이동 + val deal = deals.first() + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url)).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + val pendingIntent = PendingIntent.getActivity( + context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, CHANNEL_URGENT) + .setContentTitle(context.getString(R.string.notification_keyword_match)) + .setContentText(deal.title) + .setSmallIcon(android.R.drawable.ic_menu_send) + .setContentIntent(pendingIntent) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) + .setNumber(deals.size) // 뱃지 숫자 설정 + .build() + + notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification) + } else { + // 여러 딜인 경우 메인 화면으로 이동 + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent = PendingIntent.getActivity( + context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, CHANNEL_URGENT) + .setContentTitle(context.getString(R.string.notification_keyword_match)) + .setContentText("${deals.first().title} 외 ${deals.size - 1}개") + .setSmallIcon(android.R.drawable.ic_menu_send) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) .setStyle( NotificationCompat.BigTextStyle() .bigText(deals.take(5).joinToString("\n") { "• ${it.title}" }) ) + .setNumber(deals.size) // 뱃지 숫자 설정 .build() - - notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification) + + notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification) + } } /** diff --git a/app/src/main/java/com/hotdeal/alarm/ui/theme/Typography.kt b/app/src/main/java/com/hotdeal/alarm/ui/theme/Typography.kt index 762e58d..fba9275 100644 --- a/app/src/main/java/com/hotdeal/alarm/ui/theme/Typography.kt +++ b/app/src/main/java/com/hotdeal/alarm/ui/theme/Typography.kt @@ -6,18 +6,25 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp +/** + * Premium Typography System + * Material You 스타일의 세련된 타이포그래피 + */ + val AppTypography = Typography( - // Display + // ============================================ + // Display - 대형 헤더 (사용 빈도 낮음) + // ============================================ displayLarge = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Light, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp ), displayMedium = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, + fontWeight = FontWeight.Light, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp @@ -30,17 +37,19 @@ val AppTypography = Typography( letterSpacing = 0.sp ), - // Headline + // ============================================ + // Headline - 중형 헤더 + // ============================================ headlineLarge = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.SemiBold, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp ), headlineMedium = TextStyle( fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.SemiBold, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp @@ -53,7 +62,9 @@ val AppTypography = Typography( letterSpacing = 0.sp ), - // Title + // ============================================ + // Title - 화면/섹션 타이틀 + // ============================================ titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold, @@ -76,7 +87,9 @@ val AppTypography = Typography( letterSpacing = 0.1.sp ), - // Body + // ============================================ + // Body - 본문 텍스트 + // ============================================ bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, @@ -99,7 +112,9 @@ val AppTypography = Typography( letterSpacing = 0.4.sp ), - // Label + // ============================================ + // Label - 버튼, 칩, 뱃지 등 + // ============================================ labelLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, @@ -121,4 +136,4 @@ val AppTypography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) -) +) \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f6accbb..8e149fd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -20,4 +20,5 @@ #FF2196F3 #FFFF5722 #FF9C27B0 + #FFFFCDD2