Fix pull-to-refresh spinner position and header margins
This commit is contained in:
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = CircleShape
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
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(
|
) {
|
||||||
|
// 에러 아이콘 배경
|
||||||
|
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
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
shape = CircleShape
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ErrorOutline,
|
||||||
|
contentDescription = null,
|
||||||
|
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,
|
onClick = onRetry,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
modifier = Modifier.height(52.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
colors = ButtonDefaults.buttonColors(
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
),
|
)
|
||||||
modifier = Modifier.height(48.dp)
|
|
||||||
) {
|
) {
|
||||||
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로딩 스켈레톤 - 개선된 디자인
|
* 로딩 스켈레톤 - 프리미엄 디자인
|
||||||
*/
|
*/
|
||||||
@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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
@@ -134,7 +143,7 @@ fun DealListScreen(
|
|||||||
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 = {
|
||||||
@@ -415,16 +424,21 @@ fun DealListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull to Refresh 인디케이터 - 상단 패딩으로 잘 보이게
|
// Pull to Refresh 인디케이터 - 절대 위치로 배치하여 간격 벌어짐 방지
|
||||||
|
val progress = pullToRefreshState.progress
|
||||||
|
val showIndicator = pullToRefreshState.isRefreshing || progress > 0
|
||||||
|
|
||||||
|
if (showIndicator) {
|
||||||
PullToRefreshContainer(
|
PullToRefreshContainer(
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(top = 8.dp) // TopAppBar와 겹치지 않도록 패딩
|
.padding(top = 100.dp)
|
||||||
.zIndex(999f),
|
.zIndex(999f),
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 새로고침 트리거
|
// 새로고침 트리거
|
||||||
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -111,6 +112,7 @@ class NotificationService @Inject constructor(
|
|||||||
.setSmallIcon(android.R.drawable.ic_menu_send)
|
.setSmallIcon(android.R.drawable.ic_menu_send)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
.setNumber(deals.size) // 뱃지 숫자 설정
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
NotificationCompat.Builder(context, CHANNEL_NORMAL)
|
NotificationCompat.Builder(context, CHANNEL_NORMAL)
|
||||||
@@ -135,6 +137,30 @@ class NotificationService @Inject constructor(
|
|||||||
fun showKeywordMatchNotification(deals: List<HotDeal>) {
|
fun showKeywordMatchNotification(deals: List<HotDeal>) {
|
||||||
if (!hasNotificationPermission()) return
|
if (!hasNotificationPermission()) return
|
||||||
|
|
||||||
|
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 {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
@@ -145,7 +171,7 @@ class NotificationService @Inject constructor(
|
|||||||
|
|
||||||
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("${deals.first().title} 외 ${deals.size - 1}개")
|
||||||
.setSmallIcon(android.R.drawable.ic_menu_send)
|
.setSmallIcon(android.R.drawable.ic_menu_send)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
@@ -154,10 +180,12 @@ class NotificationService @Inject constructor(
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 알림 권한 확인
|
* 알림 권한 확인
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user