feat: UI/UX 대폭 개선 및 Pull to Refresh 스피너 수정 (v1.11.0)

### UI/UX 개선
- DealItem 프로덕션 레벨 디자인 적용
- 키워드 매칭 그라데이션 배경 효과
- 세련된 타이포그래피 및 아이콘 스타일
- 부드러운 바운스 애니메이션

### Pull to Refresh 수정
- 스피너가 레이어에 가려지는 문제 해결
- 상단 패딩 추가로 전체 스피너 표시
This commit is contained in:
sanjeok77
2026-03-07 05:20:05 +09:00
parent 4f71194e2a
commit c927cc6f9c
4 changed files with 215 additions and 164 deletions

View File

@@ -24,8 +24,8 @@ android {
applicationId = "com.hotdeal.alarm" applicationId = "com.hotdeal.alarm"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 16 versionCode = 17
versionName = "1.10.0" versionName = "1.11.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -16,14 +16,16 @@ 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.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.hotdeal.alarm.domain.model.HotDeal import com.hotdeal.alarm.domain.model.HotDeal
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
@@ -37,8 +39,9 @@ fun DealItem(
) { ) {
val context = LocalContext.current val context = LocalContext.current
// 부드러운 바운스 애니메이션
val favoriteScale by animateFloatAsState( val favoriteScale by animateFloatAsState(
targetValue = if (deal.isFavorite) 1.2f else 1f, targetValue = if (deal.isFavorite) 1.15f else 1f,
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow stiffness = Spring.StiffnessLow
@@ -48,10 +51,10 @@ fun DealItem(
val siteColor = getSiteColor(deal.siteType) val siteColor = getSiteColor(deal.siteType)
// 키워드 매칭 강조 - 옅은 빨간색 배경으로 단순화 // 키워드 매칭 배경 - 그라데이션 효과
val cardColors = if (deal.isKeywordMatch) { val cardColors = if (deal.isKeywordMatch) {
CardDefaults.elevatedCardColors( CardDefaults.elevatedCardColors(
containerColor = Color(0xFFFFEBEE) // 옅은 빨간색 배경 (Material Red 50) containerColor = Color.Transparent
) )
} else { } else {
CardDefaults.elevatedCardColors( CardDefaults.elevatedCardColors(
@@ -59,12 +62,39 @@ fun DealItem(
) )
} }
Box(modifier = modifier) {
if (deal.isKeywordMatch) {
// 키워드 매칭: 그라데이션 배경
Box(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clip(RoundedCornerShape(20.dp))
.background(
Brush.linearGradient(
colors = listOf(
Color(0xFFFFEBEE),
Color(0xFFFFCDD2).copy(alpha = 0.4f)
)
)
)
)
}
ElevatedCard( ElevatedCard(
modifier = modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.then(
if (deal.isKeywordMatch) {
Modifier.background(Color.Transparent)
} else {
Modifier
}
),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = cardColors, colors = cardColors,
elevation = CardDefaults.elevatedCardElevation( elevation = CardDefaults.elevatedCardElevation(
defaultElevation = 2.dp defaultElevation = if (deal.isKeywordMatch) 0.dp else 2.dp
), ),
onClick = onClick onClick = onClick
) { ) {
@@ -78,9 +108,9 @@ fun DealItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 사이트 뱃지 (개선된 디자인) // 사이트 뱃지 - 개선된 디자인
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(12.dp),
color = siteColor.copy(alpha = 0.12f), color = siteColor.copy(alpha = 0.12f),
modifier = Modifier.height(28.dp) modifier = Modifier.height(28.dp)
) { ) {
@@ -96,8 +126,10 @@ fun DealItem(
) )
Text( Text(
text = deal.siteType?.displayName ?: deal.siteName, text = deal.siteType?.displayName ?: deal.siteName,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
),
color = siteColor color = siteColor
) )
} }
@@ -105,33 +137,36 @@ fun DealItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
// 게시판 이름 // 게시판 이름 - 더 세련된 스타일
Surface( Surface(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(10.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
modifier = Modifier.height(24.dp) modifier = Modifier.height(24.dp)
) { ) {
Text( Text(
text = deal.boardDisplayName, text = deal.boardDisplayName,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
) )
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 키워드 매칭 배지 - 빨간색으로 변경 // 키워드 매칭 배지 - 더 눈에 띄는 디자인
if (deal.isKeywordMatch) { if (deal.isKeywordMatch) {
Surface( Surface(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(10.dp),
color = Color(0xFFE53935), // 빨간색 color = Color(0xFFE53935),
modifier = Modifier.height(24.dp) modifier = Modifier.height(24.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 10.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Star, imageVector = Icons.Filled.Star,
@@ -141,8 +176,10 @@ fun DealItem(
) )
Text( Text(
text = "내 키워드", text = "내 키워드",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
fontSize = 11.sp
),
color = Color.White color = Color.White
) )
} }
@@ -150,9 +187,9 @@ fun DealItem(
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
} }
// 액션 버튼들 // 액션 버튼들 - 더 작고 세련된 스타일
Row( Row(
horizontalArrangement = Arrangement.spacedBy(2.dp) horizontalArrangement = Arrangement.spacedBy(0.dp)
) { ) {
// 공유 버튼 // 공유 버튼
IconButton( IconButton(
@@ -162,7 +199,7 @@ fun DealItem(
Icon( Icon(
imageVector = Icons.Outlined.Share, imageVector = Icons.Outlined.Share,
contentDescription = "공유", contentDescription = "공유",
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
@@ -180,7 +217,7 @@ fun DealItem(
tint = if (deal.isFavorite) tint = if (deal.isFavorite)
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
else else
MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
@@ -189,57 +226,68 @@ fun DealItem(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// 제목 // 제목 - 더 크고 읽기 쉬운 스타일
Text( Text(
text = deal.title, text = deal.title,
style = if (deal.isKeywordMatch) { style = if (deal.isKeywordMatch) {
MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
letterSpacing = (-0.1).sp
)
} else { } else {
MaterialTheme.typography.bodyLarge MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Normal,
fontSize = 15.sp
)
}, },
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
lineHeight = 22.sp
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
// 하단: 시간 + 추가 정보 // 하단: 시간 + 화살표
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically 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(
text = formatTime(deal.createdAt), text = formatTime(deal.createdAt),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant fontWeight = FontWeight.Medium,
fontSize = 12.sp
),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
) )
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 화살표 (클릭 유도) // 화살표 (클릭 유도) - 더 세련된 스타일
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.08f),
modifier = Modifier.size(28.dp)
) {
Box(
contentAlignment = Alignment.Center
) {
Icon( Icon(
imageVector = Icons.Outlined.ArrowForward, imageVector = Icons.Outlined.ArrowForward,
contentDescription = "이동", contentDescription = "이동",
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(14.dp)
) )
} }
} }
} }
} }
}
}
}
private fun formatTime(timestamp: Long): String { private fun formatTime(timestamp: Long): String {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()

View File

@@ -415,12 +415,13 @@ fun DealListScreen(
} }
} }
// Pull to Refresh 인디케이터 - z-index 높여서 잘림 방지 // Pull to Refresh 인디케이터 - 상단 패딩으로 잘 보이게
PullToRefreshContainer( PullToRefreshContainer(
state = pullToRefreshState, state = pullToRefreshState,
modifier = Modifier modifier = Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.zIndex(999f), // 최상위 레이어로 설정 .padding(top = 8.dp) // TopAppBar와 겹치지 않도록 패딩
.zIndex(999f),
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) )

View File

@@ -1,12 +1,14 @@
{ {
"version": "1.10.0", "version": "1.11.0",
"versionCode": 16, "versionCode": 17,
"minSdk": 31, "minSdk": 31,
"targetSdk": 35, "targetSdk": 35,
"forceUpdate": false, "forceUpdate": false,
"updateUrl": "https://git.webpluss.net/attachments/33643eaf-028a-461f-94a6-d3e9bc839ecb", "updateUrl": "https://git.webpluss.net/attachments/33643eaf-028a-461f-94a6-d3e9bc839ecb",
"changelog": [ "changelog": [
"Pull to Refresh 민감도 대폭 향상 - 1/3 거리로 조정", "UI/UX 대폭 개선 - 프로덕션 레벨 디자인",
"새로고침이 훨씬 쉽고 빠르게 작동" "Pull to Refresh 스피너 가려짐 해결",
"키워드 매칭 그라데이션 배경 효과",
"DealItem 카드 디자인 고도화"
] ]
} }