diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c95c131..458ef7c 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 = 11
- versionName = "1.5.0"
+ versionCode = 12
+ versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4130517..6276324 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,9 @@
+
+
+
(null) }
var showFavoritesOnly by remember { mutableStateOf(false) }
@@ -108,252 +123,274 @@ fun DealListScreen(
)
)
},
- contentWindowInsets = WindowInsets.systemBars
+ contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { paddingValues ->
- Column(
+ Box(
modifier = Modifier
.fillMaxSize()
- .padding(paddingValues)
+ .nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
- // 필터 메뉴
- AnimatedVisibility(
- visible = showFilterMenu,
- enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeIn(),
- exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeOut()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .consumeWindowInsets(paddingValues)
) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
- )
+ // 필터 메뉴
+ AnimatedVisibility(
+ visible = showFilterMenu,
+ enter = expandVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeIn(),
+ exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessLow)) + fadeOut()
) {
- Column(modifier = Modifier.padding(16.dp)) {
- 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
- )
- }
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
+ )
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ 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))
+ 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)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
EnhancedFilterChip(
- selected = selectedSiteFilter == siteType,
- onClick = {
- selectedSiteFilter = if (selectedSiteFilter == siteType) null else siteType
- },
- label = siteType.displayName,
- color = color
+ 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
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // 특수 필터
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // 내 키워드 필터
+ EnhancedFilterChip(
+ selected = showKeywordMatchOnly,
+ onClick = { showKeywordMatchOnly = !showKeywordMatchOnly },
+ label = "내 키워드",
+ color = MaterialTheme.colorScheme.primary
+ )
+
+ // 즐겨찾기 필터
+ EnhancedFilterChip(
+ selected = showFavoritesOnly,
+ onClick = { showFavoritesOnly = !showFavoritesOnly },
+ label = "즐겨찾기",
+ color = MaterialTheme.colorScheme.error
)
}
}
+ }
+ }
- Spacer(modifier = Modifier.height(8.dp))
-
- // 특수 필터
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- // 내 키워드 필터
- EnhancedFilterChip(
- selected = showKeywordMatchOnly,
- onClick = { showKeywordMatchOnly = !showKeywordMatchOnly },
- label = "내 키워드",
- color = MaterialTheme.colorScheme.primary
- )
-
- // 즐겨찾기 필터
- EnhancedFilterChip(
- selected = showFavoritesOnly,
- onClick = { showFavoritesOnly = !showFavoritesOnly },
- label = "즐겨찾기",
- color = MaterialTheme.colorScheme.error
- )
+ // 검색창
+ OutlinedTextField(
+ value = searchText,
+ onValueChange = { searchText = it },
+ placeholder = {
+ Text(
+ text = "제목으로 검색...",
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ singleLine = true,
+ 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) {
+ is MainUiState.Loading -> {
+ DealListSkeleton(count = 5)
+ }
+
+ is MainUiState.Success -> {
+ val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly, showKeywordMatchOnly) {
+ state.deals.filter { deal ->
+ val matchesSearch = searchText.isBlank() || deal.title.contains(searchText, ignoreCase = true)
+ val matchesSite = selectedSiteFilter == null || deal.siteType == selectedSiteFilter
+ val matchesFavorite = !showFavoritesOnly || deal.isFavorite
+ val matchesKeyword = !showKeywordMatchOnly || deal.isKeywordMatch
+ matchesSearch && matchesSite && matchesFavorite && matchesKeyword
+ }
+ }
+
+ if (filteredDeals.isEmpty()) {
+ if (state.deals.isEmpty()) {
+ NoDealsState(onRefresh = { viewModel.refresh() })
+ } else {
+ val selectedFilter = selectedSiteFilter
+ val message = when {
+ showKeywordMatchOnly -> "키워드 매칭된 핫딜이 없습니다"
+ showFavoritesOnly -> "즐겨찾기한 핫딜이 없습니다"
+ selectedFilter != null -> "${selectedFilter.displayName}의 핫딜이 없습니다"
+ searchText.isNotBlank() -> "'$searchText'에 대한 검색 결과가 없습니다"
+ else -> "표시할 핫딜이 없습니다"
+ }
+ EmptyState(
+ title = "결과가 없습니다",
+ message = message,
+ icon = Icons.Outlined.Search
+ )
+ }
+ } else {
+ 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) {
+ val filter = selectedSiteFilter!!
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = "• ${filter.displayName}",
+ style = MaterialTheme.typography.labelMedium,
+ color = getSiteColor(filter)
+ )
+ }
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(
+ items = filteredDeals,
+ key = { it.id }
+ ) { deal ->
+ AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(animationSpec = tween(300)) +
+ slideInVertically(
+ animationSpec = tween(300),
+ initialOffsetY = { it / 8 }
+ ),
+ exit = fadeOut(animationSpec = tween(200))
+ ) {
+ DealItem(
+ deal = deal,
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
+ context.startActivity(intent)
+ },
+ onFavoriteToggle = { dealId ->
+ viewModel.toggleFavorite(dealId)
+ }
+ )
+ }
+ }
+
+ // EdgeToEdge: 하단 네비게이션 바 공간 확보
+ item {
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+ }
+ }
+ }
+
+ is MainUiState.Error -> {
+ ErrorState(
+ message = state.message,
+ onRetry = { viewModel.refresh() }
+ )
}
}
}
- // 검색창
- OutlinedTextField(
- value = searchText,
- onValueChange = { searchText = it },
- placeholder = {
- Text(
- text = "제목으로 검색...",
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
- )
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- singleLine = true,
- 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)
- )
+ // Pull to Refresh 인디케이터
+ PullToRefreshContainer(
+ state = pullToRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter),
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
-
- // 딜 리스트
- when (val state = uiState) {
- is MainUiState.Loading -> {
- DealListSkeleton(count = 5)
- }
-
- is MainUiState.Success -> {
- val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly, showKeywordMatchOnly) {
- state.deals.filter { deal ->
- val matchesSearch = searchText.isBlank() ||
- deal.title.contains(searchText, ignoreCase = true)
- val matchesSite = selectedSiteFilter == null ||
- deal.siteType == selectedSiteFilter
- val matchesFavorite = !showFavoritesOnly || deal.isFavorite
- val matchesKeyword = !showKeywordMatchOnly || deal.isKeywordMatch
- matchesSearch && matchesSite && matchesFavorite && matchesKeyword
- }
- }
-
- if (filteredDeals.isEmpty()) {
- if (state.deals.isEmpty()) {
- NoDealsState(onRefresh = { viewModel.refresh() })
- } else {
- val selectedFilter = selectedSiteFilter
- val message = when {
- showKeywordMatchOnly -> "키워드 매칭된 핫딜이 없습니다"
- showFavoritesOnly -> "즐겨찾기한 핫딜이 없습니다"
- selectedFilter != null -> "${selectedFilter.displayName}의 핫딜이 없습니다"
- searchText.isNotBlank() -> "'$searchText'에 대한 검색 결과가 없습니다"
- else -> "표시할 핫딜이 없습니다"
- }
- EmptyState(
- title = "결과가 없습니다",
- message = message,
- icon = Icons.Outlined.Search
- )
- }
- } else {
- 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) {
- val filter = selectedSiteFilter!!
- Spacer(modifier = Modifier.width(8.dp))
- Text(
- text = "• ${filter.displayName}",
- style = MaterialTheme.typography.labelMedium,
- color = getSiteColor(filter)
- )
- }
- }
- }
-
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- items(
- items = filteredDeals,
- key = { it.id }
- ) { deal ->
- AnimatedVisibility(
- visible = true,
- enter = fadeIn(animationSpec = tween(300)) +
- slideInVertically(
- animationSpec = tween(300),
- initialOffsetY = { it / 8 }
- ),
- exit = fadeOut(animationSpec = tween(200))
- ) {
- DealItem(
- deal = deal,
- onClick = {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url))
- context.startActivity(intent)
- },
- onFavoriteToggle = { dealId ->
- viewModel.toggleFavorite(dealId)
- }
- )
- }
- }
-
- item {
- Spacer(modifier = Modifier.height(16.dp))
- }
- }
- }
- }
-
- is MainUiState.Error -> {
- ErrorState(
- message = state.message,
- onRetry = { viewModel.refresh() }
- )
+
+ // 새로고침 트리거
+ LaunchedEffect(pullToRefreshState.isRefreshing) {
+ if (pullToRefreshState.isRefreshing) {
+ isRefreshing = true
+ viewModel.refresh()
}
}
}
diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
index 9d4b36a..554e08e 100644
--- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
+++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainActivity.kt
@@ -9,11 +9,8 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.lifecycleScope
import com.hotdeal.alarm.ui.theme.HotDealTheme
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 9f147fd..b7adeca 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,11 +1,16 @@
-