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,183 +62,228 @@ fun DealItem(
) )
} }
ElevatedCard( Box(modifier = modifier) {
modifier = modifier.fillMaxWidth(), if (deal.isKeywordMatch) {
shape = RoundedCornerShape(20.dp), // 키워드 매칭: 그라데이션 배경
colors = cardColors, Box(
elevation = CardDefaults.elevatedCardElevation( modifier = Modifier
defaultElevation = 2.dp .fillMaxWidth()
), .height(IntrinsicSize.Min)
onClick = onClick .clip(RoundedCornerShape(20.dp))
) { .background(
Column( Brush.linearGradient(
colors = listOf(
Color(0xFFFFEBEE),
Color(0xFFFFCDD2).copy(alpha = 0.4f)
)
)
)
)
}
ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .then(
) { if (deal.isKeywordMatch) {
// 상단: 사이트 뱃지 + 게시판 + 액션 Modifier.background(Color.Transparent)
Row( } else {
modifier = Modifier.fillMaxWidth(), Modifier
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
)
} }
} ),
shape = RoundedCornerShape(20.dp),
Spacer(modifier = Modifier.width(8.dp)) colors = cardColors,
elevation = CardDefaults.elevatedCardElevation(
// 게시판 이름 defaultElevation = if (deal.isKeywordMatch) 0.dp else 2.dp
Surface( ),
shape = RoundedCornerShape(8.dp), onClick = onClick
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), ) {
modifier = Modifier.height(24.dp) Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 상단: 사이트 뱃지 + 게시판 + 액션
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( // 사이트 뱃지 - 개선된 디자인
text = deal.boardDisplayName,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
Spacer(modifier = Modifier.weight(1f))
// 키워드 매칭 배지 - 빨간색으로 변경
if (deal.isKeywordMatch) {
Surface( Surface(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(12.dp),
color = Color(0xFFE53935), // 빨간색 color = siteColor.copy(alpha = 0.12f),
modifier = Modifier.height(24.dp) modifier = Modifier.height(28.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 10.dp)
) { ) {
Icon( Box(
imageVector = Icons.Filled.Star, modifier = Modifier
contentDescription = null, .size(8.dp)
tint = Color.White, .background(siteColor, CircleShape)
modifier = Modifier.size(12.dp)
) )
Text( Text(
text = "내 키워드", text = deal.siteType?.displayName ?: deal.siteName,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.SemiBold,
color = Color.White fontSize = 13.sp
),
color = siteColor
) )
} }
} }
Spacer(modifier = Modifier.width(4.dp))
}
// 액션 버튼들 Spacer(modifier = Modifier.width(8.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp) // 게시판 이름 - 더 세련된 스타일
) { Surface(
// 공유 버튼 shape = RoundedCornerShape(10.dp),
IconButton( color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
onClick = { ShareHelper.shareDeal(context, deal) }, modifier = Modifier.height(24.dp)
modifier = Modifier.size(36.dp)
) { ) {
Icon( Text(
imageVector = Icons.Outlined.Share, text = deal.boardDisplayName,
contentDescription = "공유", style = MaterialTheme.typography.labelSmall.copy(
tint = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 11.sp,
modifier = Modifier.size(18.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))
IconButton(
onClick = { onFavoriteToggle(deal.id) }, // 키워드 매칭 배지 - 더 눈에 띄는 디자인
modifier = Modifier if (deal.isKeywordMatch) {
.size(36.dp) Surface(
.scale(favoriteScale) shape = RoundedCornerShape(10.dp),
color = Color(0xFFE53935),
modifier = Modifier.height(24.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 10.dp)
) {
Icon(
imageVector = Icons.Filled.Star,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(12.dp)
)
Text(
text = "내 키워드",
style = MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight.SemiBold,
fontSize = 11.sp
),
color = Color.White
)
}
}
Spacer(modifier = Modifier.width(4.dp))
}
// 액션 버튼들 - 더 작고 세련된 스타일
Row(
horizontalArrangement = Arrangement.spacedBy(0.dp)
) { ) {
Icon( // 공유 버튼
imageVector = if (deal.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, IconButton(
contentDescription = if (deal.isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가", onClick = { ShareHelper.shareDeal(context, deal) },
tint = if (deal.isFavorite) modifier = Modifier.size(36.dp)
MaterialTheme.colorScheme.error ) {
else Icon(
MaterialTheme.colorScheme.onSurfaceVariant, imageVector = Icons.Outlined.Share,
modifier = Modifier.size(18.dp) contentDescription = "공유",
) tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
// 즐겨찾기 버튼
IconButton(
onClick = { onFavoriteToggle(deal.id) },
modifier = Modifier
.size(36.dp)
.scale(favoriteScale)
) {
Icon(
imageVector = if (deal.isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = if (deal.isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
tint = if (deal.isFavorite)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
} }
} }
}
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(
} else { fontWeight = FontWeight.Bold,
MaterialTheme.typography.bodyLarge fontSize = 16.sp,
}, letterSpacing = (-0.1).sp
maxLines = 2, )
overflow = TextOverflow.Ellipsis, } else {
color = MaterialTheme.colorScheme.onSurface MaterialTheme.typography.bodyLarge.copy(
) fontWeight = FontWeight.Normal,
fontSize = 15.sp
)
},
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
lineHeight = 22.sp
)
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
// 하단: 시간 + 추가 정보 // 하단: 시간 + 화살표
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// 시간
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp) verticalAlignment = Alignment.CenterVertically
) { ) {
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))
// 화살표 (클릭 유도) - 더 세련된 스타일
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.08f),
modifier = Modifier.size(28.dp)
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Outlined.ArrowForward,
contentDescription = "이동",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp)
)
}
}
} }
Spacer(modifier = Modifier.weight(1f))
// 화살표 (클릭 유도)
Icon(
imageVector = Icons.Outlined.ArrowForward,
contentDescription = "이동",
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
modifier = Modifier.size(16.dp)
)
} }
} }
} }

View File

@@ -415,15 +415,16 @@ 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와 겹치지 않도록 패딩
containerColor = MaterialTheme.colorScheme.primaryContainer, .zIndex(999f),
contentColor = MaterialTheme.colorScheme.onPrimaryContainer containerColor = MaterialTheme.colorScheme.primaryContainer,
) contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
// 새로고침 트리거 // 새로고침 트리거
LaunchedEffect(pullToRefreshState.isRefreshing) { LaunchedEffect(pullToRefreshState.isRefreshing) {

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 카드 디자인 고도화"
] ]
} }