feat: EdgeToEdge 개선, Pull to Refresh 추가, 출처를 알 수 없는 앱 설치 권한 추가 (v1.6.0)

- EdgeToEdge: 투명 상태바/네비게이션바 설정, WindowInsets 처리 개선
- Pull to Refresh: 아래로 당겨서 새로고침 기능 추가
- 권한: REQUEST_INSTALL_PACKAGES 추가로 APK 업데이트 설치 지원
- 버전: 1.5.0 -> 1.6.0 (versionCode 11 -> 12)
This commit is contained in:
sanjeok77
2026-03-05 10:52:09 +09:00
parent a215f218bc
commit eee4617989
5 changed files with 281 additions and 239 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 = 11 versionCode = 12
versionName = "1.5.0" versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -20,6 +20,9 @@
<!-- Boot Completed --> <!-- Boot Completed -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Install Unknown Apps (for APK updates) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
android:name=".HotDealApplication" android:name=".HotDealApplication"
android:allowBackup="true" android:allowBackup="true"

View File

@@ -16,11 +16,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.unit.dp import androidx.compose.ui.unit.dp
@@ -40,6 +43,18 @@ fun DealListScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
// Pull to Refresh 상태
val pullToRefreshState = rememberPullToRefreshState()
var isRefreshing by remember { mutableStateOf(false) }
// 새로고침 완료 감지
LaunchedEffect(uiState) {
if (uiState !is MainUiState.Loading) {
isRefreshing = false
pullToRefreshState.endRefresh()
}
}
var searchText by remember { mutableStateOf("") } var searchText by remember { mutableStateOf("") }
var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) } var selectedSiteFilter by remember { mutableStateOf<SiteType?>(null) }
var showFavoritesOnly by remember { mutableStateOf(false) } var showFavoritesOnly by remember { mutableStateOf(false) }
@@ -108,12 +123,18 @@ fun DealListScreen(
) )
) )
}, },
contentWindowInsets = WindowInsets.systemBars contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { paddingValues -> ) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.consumeWindowInsets(paddingValues)
) { ) {
// 필터 메뉴 // 필터 메뉴
AnimatedVisibility( AnimatedVisibility(
@@ -248,10 +269,8 @@ fun DealListScreen(
is MainUiState.Success -> { is MainUiState.Success -> {
val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly, showKeywordMatchOnly) { val filteredDeals = remember(state.deals, searchText, selectedSiteFilter, showFavoritesOnly, showKeywordMatchOnly) {
state.deals.filter { deal -> state.deals.filter { deal ->
val matchesSearch = searchText.isBlank() || val matchesSearch = searchText.isBlank() || deal.title.contains(searchText, ignoreCase = true)
deal.title.contains(searchText, ignoreCase = true) val matchesSite = selectedSiteFilter == null || deal.siteType == selectedSiteFilter
val matchesSite = selectedSiteFilter == null ||
deal.siteType == selectedSiteFilter
val matchesFavorite = !showFavoritesOnly || deal.isFavorite val matchesFavorite = !showFavoritesOnly || deal.isFavorite
val matchesKeyword = !showKeywordMatchOnly || deal.isKeywordMatch val matchesKeyword = !showKeywordMatchOnly || deal.isKeywordMatch
matchesSearch && matchesSite && matchesFavorite && matchesKeyword matchesSearch && matchesSite && matchesFavorite && matchesKeyword
@@ -342,8 +361,9 @@ fun DealListScreen(
} }
} }
// EdgeToEdge: 하단 네비게이션 바 공간 확보
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(32.dp))
} }
} }
} }
@@ -357,6 +377,23 @@ fun DealListScreen(
} }
} }
} }
// Pull to Refresh 인디케이터
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
// 새로고침 트리거
LaunchedEffect(pullToRefreshState.isRefreshing) {
if (pullToRefreshState.isRefreshing) {
isRefreshing = true
viewModel.refresh()
}
}
}
} }
} }

View File

@@ -9,11 +9,8 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* 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.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.hotdeal.alarm.ui.theme.HotDealTheme import com.hotdeal.alarm.ui.theme.HotDealTheme

View File

@@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.HotDealAlarm" parent="android:Theme.Material.Light.NoActionBar"> <!-- EdgeToEdge compatible theme using AppCompat -->
<item name="android:colorPrimary">@color/primary</item> <style name="Theme.HotDealAlarm" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:colorPrimaryDark">@color/primary_dark</item> <item name="colorPrimary">@color/primary</item>
<item name="android:colorAccent">@color/secondary</item> <item name="colorPrimaryDark">@color/primary_dark</item>
<item name="android:statusBarColor">@color/primary_dark</item> <item name="colorAccent">@color/secondary</item>
<item name="android:navigationBarColor">@color/background</item> <!-- EdgeToEdge: 투명 상태바/네비게이션바 -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item> <item name="android:windowLightNavigationBar">true</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
</style> </style>
</resources> </resources>