Fix pull-to-refresh spinner position and header margins

This commit is contained in:
sanjeok77
2026-03-07 06:59:10 +09:00
parent 6715388234
commit c332af4c28
8 changed files with 327 additions and 164 deletions

View File

@@ -51,10 +51,10 @@ fun DealItem(
val siteColor = getSiteColor(deal.siteType) val siteColor = getSiteColor(deal.siteType)
// 키워드 매칭 배경 - 그라데이션 효과 // 키워드 매칭 배경 - 옅은 붉은색 단색 배경
val cardColors = if (deal.isKeywordMatch) { val cardColors = if (deal.isKeywordMatch) {
CardDefaults.elevatedCardColors( CardDefaults.elevatedCardColors(
containerColor = Color.Transparent containerColor = Color(0xFFFCE8E8) // 옅은 붉은색
) )
} else { } else {
CardDefaults.elevatedCardColors( CardDefaults.elevatedCardColors(
@@ -63,38 +63,14 @@ fun DealItem(
} }
Box(modifier = modifier) { 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( ElevatedCard(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.then(
if (deal.isKeywordMatch) {
Modifier.background(Color.Transparent)
} else {
Modifier
}
),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = cardColors, colors = cardColors,
elevation = CardDefaults.elevatedCardElevation( elevation = CardDefaults.elevatedCardElevation(
defaultElevation = if (deal.isKeywordMatch) 0.dp else 2.dp defaultElevation = 2.dp
), ),
onClick = onClick onClick = onClick
) { ) {

View File

@@ -21,10 +21,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.hotdeal.alarm.ui.theme.Spacing
/** /**
* 빈 상태 컴포넌트 - 개선된 디자인 * 빈 상태 컴포넌트 - 프리미엄 디자인
*/ */
@Composable @Composable
fun EmptyState( fun EmptyState(
@@ -37,56 +36,76 @@ fun EmptyState(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(Spacing.lg), .padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
// 아이콘 배경 // 그라데이션 배경 아이콘
Box( Box(
modifier = Modifier modifier = Modifier
.size(120.dp) .size(140.dp)
.background( .background(
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), brush = Brush.radialGradient(
CircleShape colors = listOf(
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f),
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
)
),
shape = CircleShape
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Box(
imageVector = icon, modifier = Modifier
contentDescription = null, .size(100.dp)
modifier = Modifier.size(64.dp), .background(
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) 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(
text = title, text = title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(10.dp))
Text( Text(
text = message, text = message,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
if (action != null) { if (action != null) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(28.dp))
action() action()
} }
} }
} }
/** /**
* 핫딜 없음 상태 - 개선된 디자인 * 핫딜 없음 상태 - 프리미엄 디자인
*/ */
@Composable @Composable
fun NoDealsState( fun NoDealsState(
@@ -95,25 +114,28 @@ fun NoDealsState(
) { ) {
EmptyState( EmptyState(
title = "수집된 핫딜이 없습니다", title = "수집된 핫딜이 없습니다",
message = "새로고침하여 최신 핫딜을 확인보세요", message = "새로고침하여 최신 핫딜을 확인보세요",
icon = Icons.Outlined.ShoppingBag, icon = Icons.Outlined.ShoppingBag,
modifier = modifier, modifier = modifier,
action = { action = {
FilledTonalButton( Button(
onClick = onRefresh, onClick = onRefresh,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
modifier = Modifier.height(48.dp) modifier = Modifier.height(52.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Filled.Refresh,
contentDescription = null, 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(
text = "새로고침", text = "새로고침",
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium fontWeight = FontWeight.SemiBold
) )
} }
} }
@@ -137,7 +159,7 @@ fun NoSearchResultState(
} }
/** /**
* 에러 상태 - 개선된 디자인 * 에러 상태 - 프리미엄 디자인
*/ */
@Composable @Composable
fun ErrorState( fun ErrorState(
@@ -145,39 +167,92 @@ fun ErrorState(
onRetry: () -> Unit, onRetry: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
EmptyState( Column(
title = "오류가 발생했습니다", modifier = modifier
message = message, .fillMaxSize()
icon = Icons.Outlined.ErrorOutline, .padding(32.dp),
modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally,
action = { verticalArrangement = Arrangement.Center
FilledTonalButton( ) {
onClick = onRetry, // 에러 아이콘 배경
shape = RoundedCornerShape(16.dp), Box(
colors = ButtonDefaults.filledTonalButtonColors( modifier = Modifier
containerColor = MaterialTheme.colorScheme.errorContainer, .size(140.dp)
contentColor = MaterialTheme.colorScheme.onErrorContainer .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( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Outlined.ErrorOutline,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(20.dp) modifier = Modifier.size(56.dp),
) tint = Color.White
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "다시 시도",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium
) )
} }
} }
)
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 @Composable
fun DealListSkeleton( fun DealListSkeleton(
@@ -209,6 +284,18 @@ private fun DealItemSkeleton(
visible = true 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( AnimatedVisibility(
visible = visible && isVisible, visible = visible && isVisible,
enter = fadeIn(animationSpec = tween(300)) + slideInVertically( enter = fadeIn(animationSpec = tween(300)) + slideInVertically(
@@ -220,7 +307,7 @@ private fun DealItemSkeleton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors( 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) elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) { ) {
@@ -233,82 +320,95 @@ private fun DealItemSkeleton(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 사이트 뱃지 스켈레톤 // 사이트 뱃지 스켈레톤
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.height(28.dp) .height(32.dp)
.width(80.dp) .width(90.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
// 게시판 스켈레톤 // 게시판 스켈레톤
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.height(24.dp) .height(28.dp)
.width(60.dp) .width(70.dp)
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 액션 버튼 스켈레톤 // 액션 버튼 스켈레톤
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(14.dp))
// 제목 스켈레톤 // 제목 스켈레톤
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.9f) .fillMaxWidth(0.92f)
.height(20.dp) .height(22.dp)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.6f) .fillMaxWidth(0.65f)
.height(20.dp) .height(22.dp)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(6.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(12.dp))
// 하단 스켈레톤 // 하단 스켈레톤
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.height(14.dp) .height(16.dp)
.width(60.dp) .width(70.dp)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Box( SkeletonBox(
modifier = Modifier modifier = Modifier
.size(16.dp) .size(32.dp)
.clip(CircleShape) .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)
)
)
}

View File

@@ -22,19 +22,20 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.compose.ui.draw.scale
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.hotdeal.alarm.domain.model.SiteType import com.hotdeal.alarm.domain.model.SiteType
import com.hotdeal.alarm.presentation.components.* import com.hotdeal.alarm.presentation.components.*
import com.hotdeal.alarm.presentation.main.MainUiState import com.hotdeal.alarm.presentation.main.MainUiState
import com.hotdeal.alarm.presentation.main.MainViewModel import com.hotdeal.alarm.presentation.main.MainViewModel
import com.hotdeal.alarm.ui.theme.getSiteColor import com.hotdeal.alarm.ui.theme.getSiteColor
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -73,6 +74,14 @@ fun DealListScreen(
var showKeywordMatchOnly by remember { mutableStateOf(false) } var showKeywordMatchOnly by remember { mutableStateOf(false) }
var showFilterMenu by remember { mutableStateOf(false) } var showFilterMenu by remember { mutableStateOf(false) }
// 필터 메뉴 자동 닫기 (5초)
LaunchedEffect(showFilterMenu) {
if (showFilterMenu) {
delay(5000L)
showFilterMenu = false
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -130,13 +139,13 @@ fun DealListScreen(
) )
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface scrolledContainerColor = MaterialTheme.colorScheme.surface
), ),
modifier = Modifier.statusBarsPadding() windowInsets = WindowInsets(0, 0, 0, 0)
) )
}, },
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
visible = showScrollToTop, visible = showScrollToTop,
@@ -150,7 +159,7 @@ fun DealListScreen(
containerColor = MaterialTheme.colorScheme.primary, containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary, contentColor = MaterialTheme.colorScheme.onPrimary,
shape = CircleShape, shape = CircleShape,
modifier = Modifier.navigationBarsPadding() modifier = Modifier.navigationBarsPadding()
) { ) {
Icon( Icon(
imageVector = Icons.Filled.KeyboardArrowUp, imageVector = Icons.Filled.KeyboardArrowUp,
@@ -398,10 +407,10 @@ fun DealListScreen(
} }
} }
// EdgeToEdge: 하단 네비게이션 바 공간 확보 // EdgeToEdge: 하단 네비게이션 바 공간 확보
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
} }
} }
} }
@@ -415,16 +424,21 @@ fun DealListScreen(
} }
} }
// Pull to Refresh 인디케이터 - 상단 패딩으로 잘 보이게 // Pull to Refresh 인디케이터 - 절대 위치로 배치하여 간격 벌어짐 방지
PullToRefreshContainer( val progress = pullToRefreshState.progress
state = pullToRefreshState, val showIndicator = pullToRefreshState.isRefreshing || progress > 0
modifier = Modifier
.align(Alignment.TopCenter) if (showIndicator) {
.padding(top = 8.dp) // TopAppBar와 겹치지 않도록 패딩 PullToRefreshContainer(
.zIndex(999f), state = pullToRefreshState,
containerColor = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier
contentColor = MaterialTheme.colorScheme.onPrimaryContainer .align(Alignment.TopCenter)
) .padding(top = 100.dp)
.zIndex(999f),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
// 새로고침 트리거 // 새로고침 트리거
LaunchedEffect(pullToRefreshState.isRefreshing) { LaunchedEffect(pullToRefreshState.isRefreshing) {

View File

@@ -137,6 +137,22 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
// One UI 7+에서 시스템 바 인셋 처리 // 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) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())

View File

@@ -12,6 +12,8 @@ import com.hotdeal.alarm.worker.WorkerScheduler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -145,6 +147,17 @@ class MainViewModel @Inject constructor(
fun stopPolling() { fun stopPolling() {
workerScheduler.cancelPolling() workerScheduler.cancelPolling()
} }
// 데이터 파싱 핫딜 데이터 전체 삭제 및 사용자 피드백 트리거
private val _toastEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
val toastEvent = _toastEvent.asSharedFlow()
fun deleteAllParsedData() {
viewModelScope.launch {
hotDealDao.deleteAllDeals()
_toastEvent.emit("파싱 데이터가 삭제되었습니다")
}
}
} }
sealed class MainUiState { sealed class MainUiState {

View File

@@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -110,8 +111,9 @@ class NotificationService @Inject constructor(
.setContentText(deals.first().title) .setContentText(deals.first().title)
.setSmallIcon(android.R.drawable.ic_menu_send) .setSmallIcon(android.R.drawable.ic_menu_send)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
.build() .setNumber(deals.size) // 뱃지 숫자 설정
.build()
} else { } else {
NotificationCompat.Builder(context, CHANNEL_NORMAL) NotificationCompat.Builder(context, CHANNEL_NORMAL)
.setContentTitle(context.getString(R.string.notification_new_deal)) .setContentTitle(context.getString(R.string.notification_new_deal))
@@ -135,28 +137,54 @@ class NotificationService @Inject constructor(
fun showKeywordMatchNotification(deals: List<HotDeal>) { fun showKeywordMatchNotification(deals: List<HotDeal>) {
if (!hasNotificationPermission()) return if (!hasNotificationPermission()) return
val intent = Intent(context, MainActivity::class.java).apply { if (deals.size == 1) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK // 단일 딜인 경우 해당 URL로 바로 이동
} val deal = deals.first()
val pendingIntent = PendingIntent.getActivity( val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url)).apply {
context, 0, intent, flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE }
) val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_URGENT) val notification = NotificationCompat.Builder(context, CHANNEL_URGENT)
.setContentTitle(context.getString(R.string.notification_keyword_match)) .setContentTitle(context.getString(R.string.notification_keyword_match))
.setContentText(deals.first().title) .setContentText(deal.title)
.setSmallIcon(android.R.drawable.ic_menu_send) .setSmallIcon(android.R.drawable.ic_menu_send)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH) .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( .setStyle(
NotificationCompat.BigTextStyle() NotificationCompat.BigTextStyle()
.bigText(deals.take(5).joinToString("\n") { "${it.title}" }) .bigText(deals.take(5).joinToString("\n") { "${it.title}" })
) )
.setNumber(deals.size) // 뱃지 숫자 설정
.build() .build()
notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification) notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification)
}
} }
/** /**

View File

@@ -6,18 +6,25 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
/**
* Premium Typography System
* Material You 스타일의 세련된 타이포그래피
*/
val AppTypography = Typography( val AppTypography = Typography(
// Display // ============================================
// Display - 대형 헤더 (사용 빈도 낮음)
// ============================================
displayLarge = TextStyle( displayLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Light,
fontSize = 57.sp, fontSize = 57.sp,
lineHeight = 64.sp, lineHeight = 64.sp,
letterSpacing = (-0.25).sp letterSpacing = (-0.25).sp
), ),
displayMedium = TextStyle( displayMedium = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Light,
fontSize = 45.sp, fontSize = 45.sp,
lineHeight = 52.sp, lineHeight = 52.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
@@ -30,17 +37,19 @@ val AppTypography = Typography(
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
// Headline // ============================================
// Headline - 중형 헤더
// ============================================
headlineLarge = TextStyle( headlineLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.SemiBold,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 40.sp, lineHeight = 40.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
headlineMedium = TextStyle( headlineMedium = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.SemiBold,
fontSize = 28.sp, fontSize = 28.sp,
lineHeight = 36.sp, lineHeight = 36.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
@@ -53,7 +62,9 @@ val AppTypography = Typography(
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
// Title // ============================================
// Title - 화면/섹션 타이틀
// ============================================
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
@@ -76,7 +87,9 @@ val AppTypography = Typography(
letterSpacing = 0.1.sp letterSpacing = 0.1.sp
), ),
// Body // ============================================
// Body - 본문 텍스트
// ============================================
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@@ -99,7 +112,9 @@ val AppTypography = Typography(
letterSpacing = 0.4.sp letterSpacing = 0.4.sp
), ),
// Label // ============================================
// Label - 버튼, 칩, 뱃지 등
// ============================================
labelLarge = TextStyle( labelLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,

View File

@@ -20,4 +20,5 @@
<color name="ruriweb">#FF2196F3</color> <color name="ruriweb">#FF2196F3</color>
<color name="coolenjoy">#FFFF5722</color> <color name="coolenjoy">#FFFF5722</color>
<color name="quasarzone">#FF9C27B0</color> <color name="quasarzone">#FF9C27B0</color>
<color name="keyword_card_background">#FFFFCDD2</color>
</resources> </resources>