From 0020a2a6d4343f52e848d021835da01b47959992 Mon Sep 17 00:00:00 2001 From: sanjeok77 Date: Wed, 4 Mar 2026 07:53:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20UI/UX=20=EB=8C=80=ED=8F=AD=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EB=B2=84=EC=A0=84=201.3.0=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아이콘 디자인 개선 (알림 벨 + 불꽃) - 메인 화면 헤더 제거로 화면 넓게 사용 - 설정 화면 알림 설정 UI 대폭 개선 - 키워드 카드 세련된 디자인 적용 - 전체 UI/UX 모던하게 고도화 - 필터 칩 애니메이션 추가 - 버전 1.2.0 -> 1.3.0 (versionCode 4 -> 5) --- app/build.gradle.kts | 4 +- .../alarm/presentation/components/DealItem.kt | 373 ++++--- .../presentation/components/EmptyState.kt | 245 ++++- .../presentation/deallist/DealListScreen.kt | 338 ++++-- .../alarm/presentation/main/MainScreen.kt | 64 +- .../presentation/settings/SettingsScreen.kt | 966 ++++++++++++++---- .../res/drawable/ic_launcher_foreground.xml | 9 +- version.json | 20 +- 8 files changed, 1498 insertions(+), 521 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 038bf30..19d0c5d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt index 0ceb514..497da9e 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt @@ -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): 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}분 전" diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt index 6f1e8f8..03fca6e 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/EmptyState.kt @@ -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) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt index 0ac6a8c..f2d866b 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt index fd13d3c..1422be8 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainScreen.kt @@ -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") } - - diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt index d14cfa6..db55878 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt @@ -1,21 +1,39 @@ package com.hotdeal.alarm.presentation.settings import android.Manifest +import android.content.Intent +import android.net.Uri import android.os.Build +import android.provider.Settings import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons 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.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.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hotdeal.alarm.domain.model.Keyword @@ -24,6 +42,7 @@ import com.hotdeal.alarm.domain.model.SiteType import com.hotdeal.alarm.presentation.components.PermissionDialog import com.hotdeal.alarm.presentation.main.MainUiState import com.hotdeal.alarm.presentation.main.MainViewModel +import com.hotdeal.alarm.ui.theme.getSiteColor import com.hotdeal.alarm.util.PermissionHelper import com.hotdeal.alarm.util.VersionManager import kotlinx.coroutines.launch @@ -56,21 +75,32 @@ fun SettingsScreen(viewModel: MainViewModel) { val hasEnabledSites = (uiState as? MainUiState.Success)?.siteConfigs?.any { it.isEnabled } ?: false LazyColumn( - modifier = Modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(vertical = 16.dp) ) { + // 상단 알림 설정 섹션 item { - PermissionCard( + NotificationSettingsHeader( permissionStatus = permissionStatus, hasEnabledSites = hasEnabledSites, onRequestPermission = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } + }, + onOpenSystemSettings = { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + context.startActivity(intent) } ) } + // 폴링 주기 설정 item { PollingIntervalCard( currentInterval = currentPollingInterval, @@ -82,13 +112,20 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } - item { Text("사이트 선택", style = MaterialTheme.typography.titleMedium) } + // 사이트 설정 섹션 + item { + SectionHeader( + title = "사이트 선택", + icon = Icons.Outlined.Language, + description = "모니터링할 사이트를 선택하세요" + ) + } when (val state = uiState) { is MainUiState.Success -> { SiteType.entries.forEach { site -> item { - SiteCard( + EnhancedSiteCard( siteType = site, configs = state.siteConfigs.filter { it.siteName == site.name }, onToggle = { key, enabled -> viewModel.toggleSiteConfig(key, enabled) } @@ -96,45 +133,35 @@ fun SettingsScreen(viewModel: MainViewModel) { } } + // 키워드 설정 섹션 item { - Spacer(modifier = Modifier.height(16.dp)) - Text("키워드 설정", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + SectionHeader( + title = "키워드 알림", + icon = Icons.Outlined.NotificationsActive, + description = "키워드를 등록하면 해당 키워드가 포함된 핫딜 알림" + ) } item { - var keywordText by remember { mutableStateOf("") } - Row(verticalAlignment = Alignment.CenterVertically) { - OutlinedTextField( - value = keywordText, - onValueChange = { keywordText = it }, - label = { Text("키워드 입력") }, - modifier = Modifier.weight(1f), - singleLine = true - ) - Spacer(modifier = Modifier.width(8.dp)) - FilledIconButton( - onClick = { - if (keywordText.isNotBlank()) { - viewModel.addKeyword(keywordText) - keywordText = "" - } - } - ) { - Icon(Icons.Default.Add, contentDescription = "추가") + KeywordInputCard( + onAdd = { keyword -> + viewModel.addKeyword(keyword) } - } + ) } - items(state.keywords) { keyword -> - KeywordItem( + items(state.keywords, key = { it.id }) { keyword -> + EnhancedKeywordCard( keyword = keyword, onToggle = { viewModel.toggleKeyword(keyword.id, !keyword.isEnabled) }, onDelete = { viewModel.deleteKeyword(keyword.id) } ) } + // 버전 정보 item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(8.dp)) VersionCard( currentVersion = VersionManager.getCurrentVersion(context), onCheckUpdate = { @@ -151,7 +178,18 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } } - else -> { item { CircularProgressIndicator() } } + else -> { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } } } @@ -166,174 +204,714 @@ fun SettingsScreen(viewModel: MainViewModel) { } @Composable -fun PermissionCard( - permissionStatus: PermissionHelper.PermissionStatus, - hasEnabledSites: Boolean, - onRequestPermission: () -> Unit +private fun NotificationSettingsHeader( + permissionStatus: PermissionHelper.PermissionStatus, + hasEnabledSites: Boolean, + onRequestPermission: () -> Unit, + onOpenSystemSettings: () -> Unit ) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(12.dp)) - - // 알림 권한 상세 안내 - Surface( - modifier = Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.medium, - color = if (permissionStatus.hasNotificationPermission) - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - else - MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.CheckCircle else Icons.Default.Warning, - contentDescription = null, - tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - modifier = Modifier.size(32.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = if (permissionStatus.hasNotificationPermission) - "키워드 매칭 시 즉시 알림을 받을 수 있습니다" - else - "키워드 매칭 알림을 받으려면 권한을 허용해주세요", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - if (!permissionStatus.hasNotificationPermission) { - Spacer(modifier = Modifier.height(12.dp)) - Button( - onClick = onRequestPermission, - modifier = Modifier.fillMaxWidth() - ) { - Icon(Icons.Default.Notifications, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("알림 권한 허용하기") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider() - Spacer(modifier = Modifier.height(16.dp)) - - // 사이트 선택 상태 - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text("사이트 선택", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) - Text( - text = if (hasEnabledSites) "모니터링할 사이트가 선택되었습니다" else "최소 1개 사이트를 선택해주세요", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } -} - -@Composable -fun PollingIntervalCard(currentInterval: Int, onIntervalChange: (Long) -> Unit) { - var selected by remember(currentInterval) { mutableStateOf(currentInterval) } - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("폴링 주기", style = MaterialTheme.typography.titleMedium) - Text("2분 권장", style = MaterialTheme.typography.bodySmall) - Spacer(modifier = Modifier.height(8.dp)) - listOf(1, 2, 5, 10, 15, 30).forEach { minutes -> - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = selected == minutes, - onClick = { - selected = minutes - onIntervalChange(minutes.toLong()) - } - ) - Text(when(minutes) { - 1 -> "1분 (빠름)" - 2 -> "2분 (권장)" - 5 -> "5분 (보통)" - 10 -> "10분 (느림)" - 15 -> "15분 (매우 느림)" - 30 -> "30분 (절전)" - else -> "${minutes}분" - }) - } - } - } - } -} - -@Composable -fun SiteCard(siteType: SiteType, configs: List, onToggle: (String, Boolean) -> Unit) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text(siteType.displayName, style = MaterialTheme.typography.titleSmall) - configs.forEach { config -> - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text(config.displayName.substringAfter(" - "), modifier = Modifier.weight(1f)) - Switch( - checked = config.isEnabled, - onCheckedChange = { onToggle(config.siteBoardKey, it) } - ) - } - } - } - } -} - -@Composable -fun KeywordItem(keyword: Keyword, onToggle: () -> Unit, onDelete: () -> Unit) { - ListItem( - headlineContent = { Text(keyword.keyword) }, - trailingContent = { - Row { - Switch(checked = keyword.isEnabled, onCheckedChange = { onToggle() }) - IconButton(onClick = onDelete) { - Icon(Icons.Default.Delete, contentDescription = "삭제") - } - } - } - ) -} - -@Composable -fun VersionCard(currentVersion: String, onCheckUpdate: () -> Unit) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + // 헤더 + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .size(44.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Notifications, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(24.dp) + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = "알림 설정", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "핫딜 알림을 받기 위한 설정", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 알림 권한 상태 카드 + NotificationStatusCard( + icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, + title = if (permissionStatus.hasNotificationPermission) "알림 권한 허용됨" else "알림 권한 필요", + description = if (permissionStatus.hasNotificationPermission) + "키워드 매칭 시 즉시 알림을 받을 수 있습니다" + else + "알림을 받으려면 권한을 허용해주세요", + isOk = permissionStatus.hasNotificationPermission, + action = if (!permissionStatus.hasNotificationPermission) { + { onRequestPermission() } + } else null, + actionLabel = "권한 허용하기", + secondaryAction = if (permissionStatus.hasNotificationPermission) { + { onOpenSystemSettings() } + } else null, + secondaryActionLabel = "시스템 설정" + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 사이트 선택 상태 + NotificationStatusCard( + icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error, + title = if (hasEnabledSites) "사이트 선택 완료" else "사이트 선택 필요", + description = if (hasEnabledSites) + "모니터링할 사이트가 선택되었습니다" + else + "최소 1개 이상의 사이트를 선택해주세요", + isOk = hasEnabledSites + ) + } + } +} + +@Composable +private fun NotificationStatusCard( + icon: ImageVector, + title: String, + description: String, + isOk: Boolean, + action: (() -> Unit)? = null, + actionLabel: String = "", + secondaryAction: (() -> Unit)? = null, + secondaryActionLabel: String = "" +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + color = if (isOk) + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + else + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column { - Text("앱 버전", style = MaterialTheme.typography.labelMedium) - Text("v$currentVersion", style = MaterialTheme.typography.titleMedium) + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, + modifier = Modifier.size(28.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (action != null || secondaryAction != null) { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (action != null) { + Button( + onClick = action, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Filled.Notifications, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(actionLabel) + } + } + if (secondaryAction != null) { + OutlinedButton( + onClick = secondaryAction, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text(secondaryActionLabel) + } + } } - Button(onClick = onCheckUpdate) { Text("업데이트 확인") } + } + } + } +} + +@Composable +private fun SectionHeader( + title: String, + icon: ImageVector, + description: String +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun PollingIntervalCard( + currentInterval: Int, + onIntervalChange: (Long) -> Unit +) { + var selected by remember(currentInterval) { mutableStateOf(currentInterval) } + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(44.dp) + .background( + MaterialTheme.colorScheme.secondaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Schedule, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(24.dp) + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Column { + Text( + text = "새로고침 주기", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "핫딜을 확인하는 간격입니다", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 선택 옵션들 + val options = listOf( + 1 to "1분" to "빠름", + 2 to "2분" to "권장", + 5 to "5분" to "보통", + 10 to "10분" to "느림", + 15 to "15분" to "매우 느림", + 30 to "30분" to "절전" + ) + + options.chunked(3).forEach { rowOptions -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowOptions.forEach { (minutes, label, subLabel) -> + PollingOptionChip( + minutes = minutes, + label = label, + subLabel = subLabel, + isSelected = selected == minutes, + onClick = { + selected = minutes + onIntervalChange(minutes.toLong()) + }, + modifier = Modifier.weight(1f) + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun PollingOptionChip( + minutes: Int, + label: String, + subLabel: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = when { + isPressed -> 0.95f + isSelected -> 1.02f + else -> 1f + }, + animationSpec = spring(stiffness = Spring.StiffnessLow), + label = "chip_scale" + ) + + Surface( + onClick = onClick, + modifier = modifier.scale(scale), + shape = RoundedCornerShape(12.dp), + color = if (isSelected) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = if (isSelected) + null + else + null, + interactionSource = interactionSource + ) { + Column( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = label, + style = MaterialTheme.typography.titleSmall, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + color = if (isSelected) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = subLabel, + style = MaterialTheme.typography.labelSmall, + color = if (isSelected) + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } +} + +@Composable +private fun EnhancedSiteCard( + siteType: SiteType, + configs: List, + onToggle: (String, Boolean) -> Unit +) { + val siteColor = getSiteColor(siteType) + val enabledCount = configs.count { it.isEnabled } + val totalCount = configs.size + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + // 사이트 헤더 + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + // 사이트 색상 인디케이터 + Box( + modifier = Modifier + .size(40.dp) + .background(siteColor.copy(alpha = 0.15f), CircleShape), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(20.dp) + .background(siteColor, CircleShape) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = siteType.displayName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + text = "$enabledCount / $totalCount 게시판 활성화", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // 진행률 표시 + if (totalCount > 0) { + Text( + text = "${(enabledCount * 100 / totalCount)}%", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = if (enabledCount > 0) siteColor else MaterialTheme.colorScheme.outline + ) + } + } + + // 게시판 목록 + if (configs.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + configs.forEach { config -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = config.displayName.substringAfter(" - "), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + Switch( + checked = config.isEnabled, + onCheckedChange = { onToggle(config.siteBoardKey, it) }, + colors = SwitchDefaults.colors( + checkedThumbColor = siteColor, + checkedTrackColor = siteColor.copy(alpha = 0.5f) + ) + ) + } + } + } + } + } +} + +@Composable +private fun KeywordInputCard( + onAdd: (String) -> Unit +) { + var keywordText by remember { mutableStateOf("") } + var isExpanded by remember { mutableStateOf(false) } + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = keywordText, + onValueChange = { keywordText = it }, + label = { Text("새 키워드 입력") }, + placeholder = { Text("예: 에어팟, 갤럭시 버즈") }, + modifier = Modifier.weight(1f), + singleLine = true, + shape = RoundedCornerShape(12.dp), + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Tag, + contentDescription = null + ) + } + ) + + Spacer(modifier = Modifier.width(12.dp)) + + FilledIconButton( + onClick = { + if (keywordText.isNotBlank()) { + onAdd(keywordText) + keywordText = "" + } + }, + modifier = Modifier.size(56.dp), + shape = RoundedCornerShape(16.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "추가", + modifier = Modifier.size(24.dp) + ) + } + } + + // 힌트 + AnimatedVisibility( + visible = keywordText.isEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Text( + text = "💡 키워드를 등록하면 해당 키워드가 포함된 핫딜이 올라올 때 알림을 받습니다", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp) + ) + } + } + } +} + +@Composable +private fun EnhancedKeywordCard( + keyword: Keyword, + onToggle: () -> Unit, + onDelete: () -> Unit +) { + val scale by animateFloatAsState( + targetValue = if (keyword.isEnabled) 1f else 0.98f, + animationSpec = spring(stiffness = Spring.StiffnessLow), + label = "keyword_scale" + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .scale(scale), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = if (keyword.isEnabled) + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + else + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation( + defaultElevation = if (keyword.isEnabled) 2.dp else 0.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 키워드 아이콘 + Box( + modifier = Modifier + .size(44.dp) + .background( + if (keyword.isEnabled) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = if (keyword.isEnabled) Icons.Filled.Tag else Icons.Outlined.Tag, + contentDescription = null, + tint = if (keyword.isEnabled) + MaterialTheme.colorScheme.onPrimary + else + MaterialTheme.colorScheme.outline, + modifier = Modifier.size(22.dp) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // 키워드 텍스트 + Column(modifier = Modifier.weight(1f)) { + Text( + text = keyword.keyword, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = if (keyword.isEnabled) + MaterialTheme.colorScheme.onSurface + else + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + ) + Text( + text = if (keyword.isEnabled) "알림 활성화됨" else "알림 비활성화", + style = MaterialTheme.typography.bodySmall, + color = if (keyword.isEnabled) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // 토글 스위치 + Switch( + checked = keyword.isEnabled, + onCheckedChange = { onToggle() }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) + ) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + // 삭제 버튼 + IconButton( + onClick = onDelete, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "삭제", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp) + ) + } + } + } +} + +@Composable +private fun VersionCard( + currentVersion: String, + onCheckUpdate: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(44.dp) + .background( + MaterialTheme.colorScheme.tertiaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiaryContainer, + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = "앱 버전", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "v$currentVersion", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + + OutlinedButton( + onClick = onCheckUpdate, + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Outlined.SystemUpdate, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text("업데이트 확인") } } } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 55836e9..5013d62 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -4,13 +4,16 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> + + + 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"/> + + 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"/> diff --git a/version.json b/version.json index 43b9935..b155494 100644 --- a/version.json +++ b/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 모던하게 고도화", + "필터 칩 애니메이션 추가" + ] }