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"
|
applicationId = "com.hotdeal.alarm"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 4
|
versionCode = 5
|
||||||
versionName = "1.2.0"
|
versionName = "1.3.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
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.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
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.text.style.TextOverflow
|
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.ui.theme.getSiteColor
|
||||||
import com.hotdeal.alarm.util.ShareHelper
|
import com.hotdeal.alarm.util.ShareHelper
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DealItem(
|
fun DealItem(
|
||||||
deal: HotDeal,
|
deal: HotDeal,
|
||||||
@@ -37,7 +38,7 @@ fun DealItem(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val favoriteScale by animateFloatAsState(
|
val favoriteScale by animateFloatAsState(
|
||||||
targetValue = if (isFavorite) 1.3f else 1f,
|
targetValue = if (isFavorite) 1.2f else 1f,
|
||||||
animationSpec = spring(
|
animationSpec = spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
@@ -45,169 +46,253 @@ fun DealItem(
|
|||||||
label = "favorite_scale"
|
label = "favorite_scale"
|
||||||
)
|
)
|
||||||
|
|
||||||
val siteColor = getSiteColor(deal.siteType)
|
val siteColor = getSiteColor(deal.siteType)
|
||||||
|
|
||||||
// 키워드 매칭된 핫딜은 더 강한 시각적 강조
|
// 키워드 매칭 강조
|
||||||
val cardColor = if (deal.isKeywordMatch) {
|
val cardColors = if (deal.isKeywordMatch) {
|
||||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
|
CardDefaults.elevatedCardColors(
|
||||||
} else {
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
MaterialTheme.colorScheme.surface
|
)
|
||||||
}
|
} else {
|
||||||
|
CardDefaults.elevatedCardColors(
|
||||||
val borderColor = if (deal.isKeywordMatch) {
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
MaterialTheme.colorScheme.primary
|
)
|
||||||
} else {
|
}
|
||||||
Color.Transparent
|
|
||||||
}
|
|
||||||
|
|
||||||
val cardModifier = if (deal.isKeywordMatch) {
|
|
||||||
modifier.fillMaxWidth()
|
|
||||||
.border(3.dp, borderColor, MaterialTheme.shapes.large)
|
|
||||||
} else {
|
|
||||||
modifier.fillMaxWidth()
|
|
||||||
}
|
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = cardModifier,
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = RoundedCornerShape(20.dp),
|
||||||
colors = CardDefaults.elevatedCardColors(containerColor = cardColor),
|
colors = cardColors,
|
||||||
elevation = CardDefaults.elevatedCardElevation(
|
elevation = CardDefaults.elevatedCardElevation(
|
||||||
defaultElevation = if (deal.isKeywordMatch) 4.dp else 2.dp
|
defaultElevation = if (deal.isKeywordMatch) 6.dp else 2.dp
|
||||||
),
|
),
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(Spacing.md)
|
.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
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
// 상단: 사이트 뱃지 + 게시판 + 액션
|
// 키워드 매칭 인디케이터 (왼쪽)
|
||||||
Row(
|
if (deal.isKeywordMatch) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Box(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
// 사이트 뱃지 (색상으로 구분)
|
// 상단: 사이트 뱃지 + 게시판 + 액션
|
||||||
Surface(
|
Row(
|
||||||
shape = RoundedCornerShape(12.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = siteColor.copy(alpha = 0.15f),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.height(26.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
// 사이트 뱃지 (개선된 디자인)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Surface(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.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)
|
||||||
) {
|
) {
|
||||||
// 색상 점
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(8.dp)
|
|
||||||
.background(siteColor, CircleShape)
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text = deal.siteType?.displayName ?: deal.siteName,
|
text = deal.boardDisplayName,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
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(
|
||||||
text = deal.boardDisplayName,
|
text = deal.title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = if (deal.isKeywordMatch) {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
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))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
// 키워드 매칭 배지 - 별 아이콘으로 변경
|
// 하단: 시간 + 추가 정보
|
||||||
if (deal.isKeywordMatch) {
|
Row(
|
||||||
Surface(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(12.dp),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
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)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
// 시간
|
||||||
imageVector = Icons.Default.Share,
|
Row(
|
||||||
contentDescription = "공유",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
modifier = Modifier.size(18.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))
|
||||||
IconButton(
|
|
||||||
onClick = { isFavorite = !isFavorite },
|
// 화살표 (클릭 유도)
|
||||||
modifier = Modifier.size(32.dp).scale(favoriteScale)
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
imageVector = Icons.Outlined.ArrowForward,
|
||||||
contentDescription = if (isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
|
contentDescription = "이동",
|
||||||
tint = if (isFavorite)
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||||
MaterialTheme.colorScheme.error
|
modifier = Modifier.size(16.dp)
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(18.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 {
|
private fun formatTime(timestamp: Long): String {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val diff = now - timestamp
|
val diff = now - timestamp
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
package com.hotdeal.alarm.presentation.components
|
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.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.ShoppingCart
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
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.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.graphics.vector.ImageVector
|
||||||
|
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
|
import com.hotdeal.alarm.ui.theme.Spacing
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 빈 상태 컴포넌트
|
* 빈 상태 컴포넌트 - 개선된 디자인
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyState(
|
fun EmptyState(
|
||||||
@@ -31,23 +41,35 @@ fun EmptyState(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
// 아이콘 배경
|
||||||
imageVector = icon,
|
Box(
|
||||||
contentDescription = null,
|
modifier = Modifier
|
||||||
modifier = Modifier.size(120.dp),
|
.size(120.dp)
|
||||||
tint = MaterialTheme.colorScheme.outlineVariant
|
.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(Spacing.md))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(Spacing.sm))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = message,
|
text = message,
|
||||||
@@ -57,14 +79,14 @@ fun EmptyState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
Spacer(modifier = Modifier.height(Spacing.lg))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 핫딜 없음 상태
|
* 핫딜 없음 상태 - 개선된 디자인
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun NoDealsState(
|
fun NoDealsState(
|
||||||
@@ -74,17 +96,25 @@ fun NoDealsState(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title = "수집된 핫딜이 없습니다",
|
title = "수집된 핫딜이 없습니다",
|
||||||
message = "새로고침하여 최신 핫딜을 확인해보세요",
|
message = "새로고침하여 최신 핫딜을 확인해보세요",
|
||||||
icon = Icons.Default.ShoppingCart,
|
icon = Icons.Outlined.ShoppingBag,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
action = {
|
action = {
|
||||||
FilledTonalButton(onClick = onRefresh) {
|
FilledTonalButton(
|
||||||
|
onClick = onRefresh,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
modifier = Modifier.height(48.dp)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Search,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = null,
|
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(
|
EmptyState(
|
||||||
title = "검색 결과가 없습니다",
|
title = "검색 결과가 없습니다",
|
||||||
message = "'$query'에 대한 결과를 찾을 수 없습니다",
|
message = "'$query'에 대한 결과를 찾을 수 없습니다",
|
||||||
icon = Icons.Default.Search,
|
icon = Icons.Outlined.Search,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 에러 상태
|
* 에러 상태 - 개선된 디자인
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorState(
|
fun ErrorState(
|
||||||
@@ -118,12 +148,167 @@ fun ErrorState(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title = "오류가 발생했습니다",
|
title = "오류가 발생했습니다",
|
||||||
message = message,
|
message = message,
|
||||||
icon = Icons.Default.ShoppingCart,
|
icon = Icons.Outlined.ErrorOutline,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
action = {
|
action = {
|
||||||
FilledTonalButton(onClick = onRetry) {
|
FilledTonalButton(
|
||||||
Text("다시 시도")
|
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.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
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.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.hotdeal.alarm.domain.model.SiteType
|
import com.hotdeal.alarm.domain.model.SiteType
|
||||||
@@ -31,7 +35,10 @@ import com.hotdeal.alarm.ui.theme.getSiteColor
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DealListScreen(viewModel: MainViewModel) {
|
fun DealListScreen(
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
onNavigateToSettings: () -> Unit = {}
|
||||||
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -40,107 +47,179 @@ fun DealListScreen(viewModel: MainViewModel) {
|
|||||||
var showFilterMenu by remember { mutableStateOf(false) }
|
var showFilterMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// 개선된 TopAppBar
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Row(
|
||||||
text = "핫딜 목록",
|
verticalAlignment = Alignment.CenterVertically
|
||||||
style = MaterialTheme.typography.headlineSmall
|
) {
|
||||||
)
|
Box(
|
||||||
},
|
modifier = Modifier
|
||||||
actions = {
|
.size(36.dp)
|
||||||
// 필터 버튼
|
.background(
|
||||||
IconButton(onClick = { showFilterMenu = !showFilterMenu }) {
|
MaterialTheme.colorScheme.primary,
|
||||||
Icon(
|
CircleShape
|
||||||
imageVector = Icons.Default.FilterList,
|
),
|
||||||
contentDescription = "필터"
|
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() }) {
|
IconButton(onClick = { viewModel.refresh() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Refresh,
|
imageVector = Icons.Outlined.Refresh,
|
||||||
contentDescription = "새로고침"
|
contentDescription = "새로고침"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = { onNavigateToSettings() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = "설정"
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 사이트 필터 칩들
|
// 사이트 필터 칩들 - 개선된 디자인
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showFilterMenu,
|
visible = showFilterMenu,
|
||||||
enter = expandVertically() + fadeIn(),
|
enter = expandVertically(
|
||||||
exit = shrinkVertically() + fadeOut()
|
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = shrinkVertically(
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessLow)
|
||||||
|
) + fadeOut()
|
||||||
) {
|
) {
|
||||||
Column(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = Spacing.md, vertical = Spacing.sm)
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
) {
|
shape = RoundedCornerShape(16.dp),
|
||||||
Text(
|
colors = CardDefaults.cardColors(
|
||||||
text = "사이트 필터",
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(bottom = Spacing.sm)
|
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
// 전체 보기 칩 + 사이트별 필터 칩 (가로 스크롤)
|
Column(
|
||||||
Row(
|
modifier = Modifier.padding(16.dp)
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState()),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(Spacing.xs)
|
|
||||||
) {
|
) {
|
||||||
// 전체 보기 칩
|
Row(
|
||||||
FilterChip(
|
verticalAlignment = Alignment.CenterVertically
|
||||||
selected = selectedSiteFilter == null,
|
) {
|
||||||
onClick = { selectedSiteFilter = null },
|
Icon(
|
||||||
label = { Text("전체") }
|
imageVector = Icons.Outlined.FilterAlt,
|
||||||
)
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
// 사이트별 필터 칩
|
modifier = Modifier.size(20.dp)
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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(
|
OutlinedTextField(
|
||||||
value = searchText,
|
value = searchText,
|
||||||
onValueChange = { searchText = it },
|
onValueChange = { searchText = it },
|
||||||
label = { Text("제목으로 검색") },
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = "제목으로 검색...",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = Spacing.md, vertical = Spacing.sm),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
singleLine = true,
|
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) {
|
when (val state = uiState) {
|
||||||
@@ -149,17 +228,12 @@ fun DealListScreen(viewModel: MainViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is MainUiState.Success -> {
|
is MainUiState.Success -> {
|
||||||
// 필터링 적용
|
|
||||||
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter) {
|
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter) {
|
||||||
state.deals.filter { deal ->
|
state.deals.filter { deal ->
|
||||||
// 검색어 필터
|
|
||||||
val matchesSearch = searchText.isBlank() ||
|
val matchesSearch = searchText.isBlank() ||
|
||||||
deal.title.contains(searchText, ignoreCase = true)
|
deal.title.contains(searchText, ignoreCase = true)
|
||||||
|
|
||||||
// 사이트 필터
|
|
||||||
val matchesSite = selectedSiteFilter == null ||
|
val matchesSite = selectedSiteFilter == null ||
|
||||||
deal.siteType == selectedSiteFilter
|
deal.siteType == selectedSiteFilter
|
||||||
|
|
||||||
matchesSearch && matchesSite
|
matchesSearch && matchesSite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,22 +251,47 @@ fun DealListScreen(viewModel: MainViewModel) {
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
title = "결과가 없습니다",
|
title = "결과가 없습니다",
|
||||||
message = message,
|
message = message,
|
||||||
icon = Icons.Default.Search
|
icon = Icons.Outlined.Search
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 핫딜 개수 표시
|
Surface(
|
||||||
Text(
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
text = "${filteredDeals.size}개의 핫딜",
|
shape = RoundedCornerShape(12.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
) {
|
||||||
modifier = Modifier.padding(horizontal = Spacing.md, vertical = Spacing.xs)
|
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(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(Spacing.md),
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(Spacing.sm)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = filteredDeals,
|
items = filteredDeals,
|
||||||
@@ -200,8 +299,12 @@ fun DealListScreen(viewModel: MainViewModel) {
|
|||||||
) { deal ->
|
) { deal ->
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = true,
|
visible = true,
|
||||||
enter = fadeIn() + slideInVertically(),
|
enter = fadeIn(animationSpec = tween(300)) +
|
||||||
exit = fadeOut() + slideOutVertically()
|
slideInVertically(
|
||||||
|
animationSpec = tween(300),
|
||||||
|
initialOffsetY = { it / 8 }
|
||||||
|
),
|
||||||
|
exit = fadeOut(animationSpec = tween(200))
|
||||||
) {
|
) {
|
||||||
DealItem(
|
DealItem(
|
||||||
deal = deal,
|
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.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.*
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.hotdeal.alarm.presentation.components.PermissionDialog
|
import com.hotdeal.alarm.presentation.components.PermissionDialog
|
||||||
import com.hotdeal.alarm.presentation.deallist.DealListScreen
|
import com.hotdeal.alarm.presentation.deallist.DealListScreen
|
||||||
import com.hotdeal.alarm.presentation.settings.SettingsScreen
|
import com.hotdeal.alarm.presentation.settings.SettingsScreen
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel,
|
viewModel: MainViewModel,
|
||||||
@@ -56,38 +49,21 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
// TopAppBar 제거 - 화면을 넓게 사용
|
||||||
topBar = {
|
NavHost(
|
||||||
TopAppBar(
|
navController = navController,
|
||||||
title = { Text("핫딜 알람") },
|
startDestination = Screen.DealList.route
|
||||||
actions = {
|
) {
|
||||||
// 설정 버튼
|
composable(Screen.DealList.route) {
|
||||||
IconButton(onClick = { navController.navigate(Screen.Settings.route) }) {
|
DealListScreen(
|
||||||
Icon(
|
viewModel = viewModel,
|
||||||
imageVector = Icons.Default.Settings,
|
onNavigateToSettings = { navController.navigate(Screen.Settings.route) }
|
||||||
contentDescription = "설정"
|
)
|
||||||
)
|
}
|
||||||
}
|
composable(Screen.Settings.route) {
|
||||||
},
|
SettingsScreen(viewModel = viewModel)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPermissionDialog) {
|
if (showPermissionDialog) {
|
||||||
PermissionDialog(
|
PermissionDialog(
|
||||||
@@ -103,5 +79,3 @@ sealed class Screen(val route: String) {
|
|||||||
object DealList : Screen("deal_list")
|
object DealList : Screen("deal_list")
|
||||||
object Settings : Screen("settings")
|
object Settings : Screen("settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,16 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
<!-- 배경 원 -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M54,54m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"/>
|
android:pathData="M54,54m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"/>
|
||||||
|
<!-- 알림 벨 아이콘 -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#1976D2"
|
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
|
<path
|
||||||
android:fillColor="#FF9800"
|
android:fillColor="#FF5722"
|
||||||
android:pathData="M54,38m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
|
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>
|
</vector>
|
||||||
|
|||||||
20
version.json
20
version.json
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"versionCode": 4,
|
"versionCode": 5,
|
||||||
"minSdk": 31,
|
"minSdk": 31,
|
||||||
"targetSdk": 35,
|
"targetSdk": 35,
|
||||||
"forceUpdate": false,
|
"forceUpdate": false,
|
||||||
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
|
"updateUrl": "https://git.webpluss.net/sanjeok77/hotdeal_alarm/releases",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"프로덕션 수준 UI/UX 개선",
|
"아이콘 디자인 개선 (알림 벨 + 불꽃)",
|
||||||
"업데이트 체크 기능 강화",
|
"메인 화면 헤더 제거로 화면 넓게 사용",
|
||||||
"헤더 제거로 화면 공간 확보",
|
"설정 화면 알림 설정 UI 대폭 개선",
|
||||||
"리마인더 알림 설정 추가",
|
"키워드 카드 세련된 디자인 적용",
|
||||||
"버전 동기화 수정"
|
"전체 UI/UX 모던하게 고도화",
|
||||||
]
|
"필터 칩 애니메이션 추가"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user