feat: UI/UX 대폭 개선 및 버전 1.3.0 업데이트
- 아이콘 디자인 개선 (알림 벨 + 불꽃) - 메인 화면 헤더 제거로 화면 넓게 사용 - 설정 화면 알림 설정 UI 대폭 개선 - 키워드 카드 세련된 디자인 적용 - 전체 UI/UX 모던하게 고도화 - 필터 칩 애니메이션 추가 - 버전 1.2.0 -> 1.3.0 (versionCode 4 -> 5)
This commit is contained in:
@@ -24,8 +24,8 @@ android {
|
||||
applicationId = "com.hotdeal.alarm"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 4
|
||||
versionName = "1.2.0"
|
||||
versionCode = 5
|
||||
versionName = "1.3.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -8,16 +8,16 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.Star
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -27,6 +27,7 @@ import com.hotdeal.alarm.ui.theme.Spacing
|
||||
import com.hotdeal.alarm.ui.theme.getSiteColor
|
||||
import com.hotdeal.alarm.util.ShareHelper
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DealItem(
|
||||
deal: HotDeal,
|
||||
@@ -35,183 +36,267 @@ fun DealItem(
|
||||
) {
|
||||
var isFavorite by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
|
||||
|
||||
val favoriteScale by animateFloatAsState(
|
||||
targetValue = if (isFavorite) 1.3f else 1f,
|
||||
targetValue = if (isFavorite) 1.2f else 1f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "favorite_scale"
|
||||
)
|
||||
|
||||
val siteColor = getSiteColor(deal.siteType)
|
||||
|
||||
// 키워드 매칭된 핫딜은 더 강한 시각적 강조
|
||||
val cardColor = if (deal.isKeywordMatch) {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
val siteColor = getSiteColor(deal.siteType)
|
||||
|
||||
val borderColor = if (deal.isKeywordMatch) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
// 키워드 매칭 강조
|
||||
val cardColors = if (deal.isKeywordMatch) {
|
||||
CardDefaults.elevatedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
)
|
||||
} else {
|
||||
CardDefaults.elevatedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
}
|
||||
|
||||
val cardModifier = if (deal.isKeywordMatch) {
|
||||
modifier.fillMaxWidth()
|
||||
.border(3.dp, borderColor, MaterialTheme.shapes.large)
|
||||
} else {
|
||||
modifier.fillMaxWidth()
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = cardModifier,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = cardColor),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = cardColors,
|
||||
elevation = CardDefaults.elevatedCardElevation(
|
||||
defaultElevation = if (deal.isKeywordMatch) 4.dp else 2.dp
|
||||
defaultElevation = if (deal.isKeywordMatch) 6.dp else 2.dp
|
||||
),
|
||||
onClick = onClick
|
||||
) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Spacing.md)
|
||||
) {
|
||||
// 상단: 사이트 뱃지 + 게시판 + 액션
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 사이트 뱃지 (색상으로 구분)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = siteColor.copy(alpha = 0.15f),
|
||||
modifier = Modifier.height(26.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||
) {
|
||||
// 색상 점
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(siteColor, CircleShape)
|
||||
.then(
|
||||
if (deal.isKeywordMatch) {
|
||||
Modifier.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.05f)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
// 키워드 매칭 인디케이터 (왼쪽)
|
||||
if (deal.isKeywordMatch) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.width(4.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.then(
|
||||
if (deal.isKeywordMatch) {
|
||||
Modifier.padding(start = 8.dp)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
// 상단: 사이트 뱃지 + 게시판 + 액션
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 사이트 뱃지 (개선된 디자인)
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = siteColor.copy(alpha = 0.12f),
|
||||
modifier = Modifier.height(28.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(siteColor, CircleShape)
|
||||
)
|
||||
Text(
|
||||
text = deal.siteType?.displayName ?: deal.siteName,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = siteColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// 게시판 이름
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = deal.siteType?.displayName ?: deal.siteName,
|
||||
text = deal.boardDisplayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = siteColor
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 키워드 매칭 배지
|
||||
if (deal.isKeywordMatch) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.height(28.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "내 키워드",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
// 액션 버튼들
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// 공유 버튼
|
||||
IconButton(
|
||||
onClick = { ShareHelper.shareDeal(context, deal) },
|
||||
modifier = Modifier.size(36.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Share,
|
||||
contentDescription = "공유",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 즐겨찾기 버튼
|
||||
IconButton(
|
||||
onClick = { isFavorite = !isFavorite },
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.scale(favoriteScale)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
|
||||
contentDescription = if (isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
|
||||
tint = if (isFavorite)
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(Spacing.sm))
|
||||
|
||||
// 게시판 이름
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 제목
|
||||
Text(
|
||||
text = deal.boardDisplayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
text = deal.title,
|
||||
style = if (deal.isKeywordMatch) {
|
||||
MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
|
||||
} else {
|
||||
MaterialTheme.typography.bodyLarge
|
||||
},
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 키워드 매칭 배지 - 별 아이콘으로 변경
|
||||
if (deal.isKeywordMatch) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.height(26.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "내 키워드",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(Spacing.xs))
|
||||
}
|
||||
|
||||
// 공유 버튼
|
||||
IconButton(
|
||||
onClick = { ShareHelper.shareDeal(context, deal) },
|
||||
modifier = Modifier.size(32.dp)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// 하단: 시간 + 추가 정보
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 시간
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Schedule,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = formatTime(deal.createdAt),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 화살표 (클릭 유도)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "공유",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 즐겨찾기 버튼
|
||||
IconButton(
|
||||
onClick = { isFavorite = !isFavorite },
|
||||
modifier = Modifier.size(32.dp).scale(favoriteScale)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
contentDescription = if (isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
|
||||
tint = if (isFavorite)
|
||||
MaterialTheme.colorScheme.error
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
imageVector = Icons.Outlined.ArrowForward,
|
||||
contentDescription = "이동",
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.sm))
|
||||
|
||||
// 제목 (키워드 매칭 시 굵게)
|
||||
Text(
|
||||
text = deal.title,
|
||||
style = if (deal.isKeywordMatch) {
|
||||
MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
|
||||
} else {
|
||||
MaterialTheme.typography.bodyLarge
|
||||
},
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.sm))
|
||||
|
||||
// 시간
|
||||
Text(
|
||||
text = formatTime(deal.createdAt),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Brush import를 위해 추가
|
||||
private fun Modifier.background(brush: Brush, shape: Shape): Modifier {
|
||||
return this.then(
|
||||
androidx.compose.ui.draw.drawBehind {
|
||||
drawRect(brush)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private object Brush {
|
||||
fun horizontalGradient(colors: List<Color>): androidx.compose.ui.graphics.Brush {
|
||||
return androidx.compose.ui.graphics.Brush.horizontalGradient(colors)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(timestamp: Long): String {
|
||||
val now = System.currentTimeMillis()
|
||||
val diff = now - timestamp
|
||||
|
||||
|
||||
return when {
|
||||
diff < 60_000 -> "방금 전"
|
||||
diff < 3_600_000 -> "${diff / 60_000}분 전"
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
package com.hotdeal.alarm.presentation.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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(
|
||||
@@ -31,40 +41,52 @@ fun EmptyState(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(120.dp),
|
||||
tint = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.md))
|
||||
|
||||
// 아이콘 배경
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(120.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f),
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.sm))
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
|
||||
if (action != null) {
|
||||
Spacer(modifier = Modifier.height(Spacing.lg))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 핫딜 없음 상태
|
||||
* 핫딜 없음 상태 - 개선된 디자인
|
||||
*/
|
||||
@Composable
|
||||
fun NoDealsState(
|
||||
@@ -74,17 +96,25 @@ fun NoDealsState(
|
||||
EmptyState(
|
||||
title = "수집된 핫딜이 없습니다",
|
||||
message = "새로고침하여 최신 핫딜을 확인해보세요",
|
||||
icon = Icons.Default.ShoppingCart,
|
||||
icon = Icons.Outlined.ShoppingBag,
|
||||
modifier = modifier,
|
||||
action = {
|
||||
FilledTonalButton(onClick = onRefresh) {
|
||||
FilledTonalButton(
|
||||
onClick = onRefresh,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "새로고침",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.width(Spacing.xs))
|
||||
Text("새로고침")
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -101,13 +131,13 @@ fun NoSearchResultState(
|
||||
EmptyState(
|
||||
title = "검색 결과가 없습니다",
|
||||
message = "'$query'에 대한 결과를 찾을 수 없습니다",
|
||||
icon = Icons.Default.Search,
|
||||
icon = Icons.Outlined.Search,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 상태
|
||||
* 에러 상태 - 개선된 디자인
|
||||
*/
|
||||
@Composable
|
||||
fun ErrorState(
|
||||
@@ -118,12 +148,167 @@ fun ErrorState(
|
||||
EmptyState(
|
||||
title = "오류가 발생했습니다",
|
||||
message = message,
|
||||
icon = Icons.Default.ShoppingCart,
|
||||
icon = Icons.Outlined.ErrorOutline,
|
||||
modifier = modifier,
|
||||
action = {
|
||||
FilledTonalButton(onClick = onRetry) {
|
||||
Text("다시 시도")
|
||||
FilledTonalButton(
|
||||
onClick = onRetry,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "다시 시도",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 스켈레톤 - 개선된 디자인
|
||||
*/
|
||||
@Composable
|
||||
fun DealListSkeleton(
|
||||
count: Int = 5,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
repeat(count) { index ->
|
||||
DealItemSkeleton(
|
||||
isVisible = true,
|
||||
delayMillis = index * 100
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DealItemSkeleton(
|
||||
isVisible: Boolean,
|
||||
delayMillis: Int = 0
|
||||
) {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
kotlinx.coroutines.delay(delayMillis.toLong())
|
||||
visible = true
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible && isVisible,
|
||||
enter = fadeIn(animationSpec = tween(300)) + slideInVertically(
|
||||
animationSpec = tween(300),
|
||||
initialOffsetY = { it / 4 }
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// 상단 스켈레톤
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 사이트 뱃지 스켈레톤
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(28.dp)
|
||||
.width(80.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// 게시판 스켈레톤
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.width(60.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 액션 버튼 스켈레톤
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 제목 스켈레톤
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(20.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.6f)
|
||||
.height(20.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// 하단 스켈레톤
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(14.dp)
|
||||
.width(60.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,27 @@ package com.hotdeal.alarm.presentation.deallist
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.hotdeal.alarm.domain.model.SiteType
|
||||
@@ -31,7 +35,10 @@ import com.hotdeal.alarm.ui.theme.getSiteColor
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DealListScreen(viewModel: MainViewModel) {
|
||||
fun DealListScreen(
|
||||
viewModel: MainViewModel,
|
||||
onNavigateToSettings: () -> Unit = {}
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -40,107 +47,179 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
var showFilterMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// 개선된 TopAppBar
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "핫딜 목록",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
// 필터 버튼
|
||||
IconButton(onClick = { showFilterMenu = !showFilterMenu }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterList,
|
||||
contentDescription = "필터"
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Notifications,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "핫딜 알람",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
// 새로고침 버튼
|
||||
},
|
||||
actions = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (selectedSiteFilter != null) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
IconButton(onClick = { showFilterMenu = !showFilterMenu }) {
|
||||
Icon(
|
||||
imageVector = if (showFilterMenu) Icons.Filled.FilterList else Icons.Outlined.FilterList,
|
||||
contentDescription = "필터"
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { viewModel.refresh() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = "새로고침"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { onNavigateToSettings() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = "설정"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
|
||||
// 사이트 필터 칩들
|
||||
// 사이트 필터 칩들 - 개선된 디자인
|
||||
AnimatedVisibility(
|
||||
visible = showFilterMenu,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
enter = expandVertically(
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||
) + fadeIn(),
|
||||
exit = shrinkVertically(
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||
) + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Spacing.md, vertical = Spacing.sm)
|
||||
) {
|
||||
Text(
|
||||
text = "사이트 필터",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = Spacing.sm)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
|
||||
// 전체 보기 칩 + 사이트별 필터 칩 (가로 스크롤)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(Spacing.xs)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// 전체 보기 칩
|
||||
FilterChip(
|
||||
selected = selectedSiteFilter == null,
|
||||
onClick = { selectedSiteFilter = null },
|
||||
label = { Text("전체") }
|
||||
)
|
||||
|
||||
// 사이트별 필터 칩
|
||||
SiteType.entries.forEach { siteType ->
|
||||
val color = getSiteColor(siteType)
|
||||
FilterChip(
|
||||
selected = selectedSiteFilter == siteType,
|
||||
onClick = {
|
||||
selectedSiteFilter = if (selectedSiteFilter == siteType) null else siteType
|
||||
},
|
||||
label = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(color, MaterialTheme.shapes.small)
|
||||
)
|
||||
Text(siteType.displayName)
|
||||
}
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = color.copy(alpha = 0.2f),
|
||||
selectedLabelColor = color
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterAlt,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "사이트 필터",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
EnhancedFilterChip(
|
||||
selected = selectedSiteFilter == null,
|
||||
onClick = { selectedSiteFilter = null },
|
||||
label = "전체",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
SiteType.entries.forEach { siteType ->
|
||||
val color = getSiteColor(siteType)
|
||||
EnhancedFilterChip(
|
||||
selected = selectedSiteFilter == siteType,
|
||||
onClick = {
|
||||
selectedSiteFilter = if (selectedSiteFilter == siteType) null else siteType
|
||||
},
|
||||
label = siteType.displayName,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 검색창
|
||||
// 검색창 - 개선된 디자인
|
||||
OutlinedTextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
label = { Text("제목으로 검색") },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "제목으로 검색...",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Spacing.md, vertical = Spacing.sm),
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
contentDescription = "검색",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (searchText.isNotEmpty()) {
|
||||
IconButton(onClick = { searchText = "" }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = "지우기",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
)
|
||||
|
||||
when (val state = uiState) {
|
||||
@@ -149,17 +228,12 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
|
||||
is MainUiState.Success -> {
|
||||
// 필터링 적용
|
||||
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter) {
|
||||
state.deals.filter { deal ->
|
||||
// 검색어 필터
|
||||
val matchesSearch = searchText.isBlank() ||
|
||||
val matchesSearch = searchText.isBlank() ||
|
||||
deal.title.contains(searchText, ignoreCase = true)
|
||||
|
||||
// 사이트 필터
|
||||
val matchesSite = selectedSiteFilter == null ||
|
||||
val matchesSite = selectedSiteFilter == null ||
|
||||
deal.siteType == selectedSiteFilter
|
||||
|
||||
matchesSearch && matchesSite
|
||||
}
|
||||
}
|
||||
@@ -177,22 +251,47 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
EmptyState(
|
||||
title = "결과가 없습니다",
|
||||
message = message,
|
||||
icon = Icons.Default.Search
|
||||
icon = Icons.Outlined.Search
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 핫딜 개수 표시
|
||||
Text(
|
||||
text = "${filteredDeals.size}개의 핫딜",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = Spacing.md, vertical = Spacing.xs)
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Inventory2,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "${filteredDeals.size}개의 핫딜",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (selectedSiteFilter != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "• ${selectedSiteFilter.displayName}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = getSiteColor(selectedSiteFilter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(Spacing.md),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.sm)
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(
|
||||
items = filteredDeals,
|
||||
@@ -200,8 +299,12 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
) { deal ->
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = fadeOut() + slideOutVertically()
|
||||
enter = fadeIn(animationSpec = tween(300)) +
|
||||
slideInVertically(
|
||||
animationSpec = tween(300),
|
||||
initialOffsetY = { it / 8 }
|
||||
),
|
||||
exit = fadeOut(animationSpec = tween(200))
|
||||
) {
|
||||
DealItem(
|
||||
deal = deal,
|
||||
@@ -212,6 +315,10 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,3 +332,48 @@ fun DealListScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnhancedFilterChip(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
color: Color
|
||||
) {
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (selected) 1.05f else 1f,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||
label = "chip_scale"
|
||||
)
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.scale(scale),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (selected) color else MaterialTheme.colorScheme.surface,
|
||||
border = if (!selected) androidx.compose.foundation.BorderStroke(1.dp, color.copy(alpha = 0.3f)) else null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
||||
color = if (selected) Color.White else color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,27 +6,20 @@ import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.hotdeal.alarm.presentation.components.PermissionDialog
|
||||
import com.hotdeal.alarm.presentation.deallist.DealListScreen
|
||||
import com.hotdeal.alarm.presentation.settings.SettingsScreen
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel,
|
||||
@@ -34,7 +27,7 @@ fun MainScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
|
||||
var showPermissionDialog by remember { mutableStateOf(false) }
|
||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
@@ -43,7 +36,7 @@ fun MainScreen(
|
||||
showPermissionDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
@@ -55,40 +48,23 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("핫딜 알람") },
|
||||
actions = {
|
||||
// 설정 버튼
|
||||
IconButton(onClick = { navController.navigate(Screen.Settings.route) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "설정"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.DealList.route,
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
composable(Screen.DealList.route) {
|
||||
DealListScreen(viewModel = viewModel)
|
||||
}
|
||||
composable(Screen.Settings.route) {
|
||||
SettingsScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TopAppBar 제거 - 화면을 넓게 사용
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.DealList.route
|
||||
) {
|
||||
composable(Screen.DealList.route) {
|
||||
DealListScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToSettings = { navController.navigate(Screen.Settings.route) }
|
||||
)
|
||||
}
|
||||
composable(Screen.Settings.route) {
|
||||
SettingsScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
if (showPermissionDialog) {
|
||||
PermissionDialog(
|
||||
title = "알림 권한 필요",
|
||||
@@ -103,5 +79,3 @@ sealed class Screen(val route: String) {
|
||||
object DealList : Screen("deal_list")
|
||||
object Settings : Screen("settings")
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,16 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- 배경 원 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,54m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"/>
|
||||
<!-- 알림 벨 아이콘 -->
|
||||
<path
|
||||
android:fillColor="#1976D2"
|
||||
android:pathData="M54,30L54,54L74,54A20,20 0,0 0,54 34L54,30A24,24 0,0 1,78 54L54,54L54,78A24,24 0,0 1,30 54A24,24 0,0 1,54 30Z"/>
|
||||
android:pathData="M54,28C52.9,28 52,28.9 52,30L52,32C47.6,33.1 44,37.1 44,42L44,54L40,58L40,60L68,60L68,58L64,54L64,42C64,37.1 60.4,33.1 56,32L56,30C56,28.9 55.1,28 54,28ZM50,62C50,64.2 51.8,66 54,66C56.2,66 58,64.2 58,62L50,62Z"/>
|
||||
<!-- 핫딜 느낌의 불꽃 -->
|
||||
<path
|
||||
android:fillColor="#FF9800"
|
||||
android:pathData="M54,38m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
|
||||
android:fillColor="#FF5722"
|
||||
android:pathData="M70,38C70,38 72,42 72,46C72,50 68,52 68,52C68,52 70,48 68,44C66,40 70,38 70,38Z"/>
|
||||
</vector>
|
||||
|
||||
20
version.json
20
version.json
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "1.2.0",
|
||||
"versionCode": 4,
|
||||
"version": "1.3.0",
|
||||
"versionCode": 5,
|
||||
"minSdk": 31,
|
||||
"targetSdk": 35,
|
||||
"forceUpdate": false,
|
||||
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
|
||||
"changelog": [
|
||||
"프로덕션 수준 UI/UX 개선",
|
||||
"업데이트 체크 기능 강화",
|
||||
"헤더 제거로 화면 공간 확보",
|
||||
"리마인더 알림 설정 추가",
|
||||
"버전 동기화 수정"
|
||||
]
|
||||
}
|
||||
"changelog": [
|
||||
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
|
||||
"메인 화면 헤더 제거로 화면 넓게 사용",
|
||||
"설정 화면 알림 설정 UI 대폭 개선",
|
||||
"키워드 카드 세련된 디자인 적용",
|
||||
"전체 UI/UX 모던하게 고도화",
|
||||
"필터 칩 애니메이션 추가"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user