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"
minSdk = 31
targetSdk = 35
versionCode = 16
versionName = "1.10.0"
versionCode = 17
versionName = "1.11.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -16,14 +16,16 @@ 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.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.util.ShareHelper
@@ -37,8 +39,9 @@ fun DealItem(
) {
val context = LocalContext.current
// 부드러운 바운스 애니메이션
val favoriteScale by animateFloatAsState(
targetValue = if (deal.isFavorite) 1.2f else 1f,
targetValue = if (deal.isFavorite) 1.15f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
@@ -48,10 +51,10 @@ fun DealItem(
val siteColor = getSiteColor(deal.siteType)
// 키워드 매칭 강조 - 옅은 빨간색 배경으로 단순화
// 키워드 매칭 배경 - 그라데이션 효과
val cardColors = if (deal.isKeywordMatch) {
CardDefaults.elevatedCardColors(
containerColor = Color(0xFFFFEBEE) // 옅은 빨간색 배경 (Material Red 50)
containerColor = Color.Transparent
)
} else {
CardDefaults.elevatedCardColors(
@@ -59,183 +62,228 @@ fun DealItem(
)
}
ElevatedCard(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = cardColors,
elevation = CardDefaults.elevatedCardElevation(
defaultElevation = 2.dp
),
onClick = onClick
) {
Column(
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(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 상단: 사이트 뱃지 + 게시판 + 액션
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
)
.then(
if (deal.isKeywordMatch) {
Modifier.background(Color.Transparent)
} else {
Modifier
}
}
Spacer(modifier = Modifier.width(8.dp))
// 게시판 이름
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.height(24.dp)
),
shape = RoundedCornerShape(20.dp),
colors = cardColors,
elevation = CardDefaults.elevatedCardElevation(
defaultElevation = if (deal.isKeywordMatch) 0.dp else 2.dp
),
onClick = onClick
) {
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(
shape = RoundedCornerShape(8.dp),
color = Color(0xFFE53935), // 빨간색
modifier = Modifier.height(24.dp)
shape = RoundedCornerShape(12.dp),
color = siteColor.copy(alpha = 0.12f),
modifier = Modifier.height(28.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 8.dp)
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(horizontal = 10.dp)
) {
Icon(
imageVector = Icons.Filled.Star,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(12.dp)
Box(
modifier = Modifier
.size(8.dp)
.background(siteColor, CircleShape)
)
Text(
text = "내 키워드",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium,
color = Color.White
text = deal.siteType?.displayName ?: deal.siteName,
style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
),
color = siteColor
)
}
}
Spacer(modifier = Modifier.width(4.dp))
}
// 액션 버튼들
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
// 공유 버튼
IconButton(
onClick = { ShareHelper.shareDeal(context, deal) },
modifier = Modifier.size(36.dp)
Spacer(modifier = Modifier.width(8.dp))
// 게시판 이름 - 더 세련된 스타일
Surface(
shape = RoundedCornerShape(10.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
modifier = Modifier.height(24.dp)
) {
Icon(
imageVector = Icons.Outlined.Share,
contentDescription = "공유",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp)
Text(
text = deal.boardDisplayName,
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 11.sp,
fontWeight = FontWeight.Medium
),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
)
}
// 즐겨찾기 버튼
IconButton(
onClick = { onFavoriteToggle(deal.id) },
modifier = Modifier
.size(36.dp)
.scale(favoriteScale)
Spacer(modifier = Modifier.weight(1f))
// 키워드 매칭 배지 - 더 눈에 띄는 디자인
if (deal.isKeywordMatch) {
Surface(
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,
contentDescription = if (deal.isFavorite) "즐겨찾기 제거" else "즐겨찾기 추가",
tint = if (deal.isFavorite)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp)
)
// 공유 버튼
IconButton(
onClick = { ShareHelper.shareDeal(context, deal) },
modifier = Modifier.size(36.dp)
) {
Icon(
imageVector = Icons.Outlined.Share,
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 = 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
)
// 제목 - 더 크고 읽기 쉬운 스타일
Text(
text = deal.title,
style = if (deal.isKeywordMatch) {
MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
letterSpacing = (-0.1).sp
)
} else {
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(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
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
style = MaterialTheme.typography.labelSmall.copy(
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 높여서 잘림 방지
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(999f), // 최상위 레이어로 설정
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
// Pull to Refresh 인디케이터 - 상단 패딩으로 잘 보이게
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 8.dp) // TopAppBar와 겹치지 않도록 패딩
.zIndex(999f),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
// 새로고침 트리거
LaunchedEffect(pullToRefreshState.isRefreshing) {

View File

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