feat: 설정 페이지 탭 구조화 및 EdgeToEdge 개선 (v1.8.0)

- 설정 페이지를 3개 탭으로 구분 (알림, 사이트, 기타)
- 탭 간 스와이프 이동 지원
- One UI 7+ EdgeToEdge 문제 해결 (시스템 바 침범 수정)
- 파싱 데이터 삭제 기능 추가
- 업데이트 확인 시 즉시 다운로드 및 설치 기능 구현
This commit is contained in:
sanjeok77
2026-03-07 03:37:54 +09:00
parent 378a8c09a8
commit bf2084626b
4 changed files with 512 additions and 238 deletions

View File

@@ -24,9 +24,8 @@ android {
applicationId = "com.hotdeal.alarm" applicationId = "com.hotdeal.alarm"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 13 versionCode = 14
versionName = "1.7.0" versionName = "1.8.0"
versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -2,6 +2,9 @@ package com.hotdeal.alarm.presentation.main
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -11,6 +14,9 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
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
@@ -32,9 +38,11 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge()
// 2분 간격으로 폴<> 시작 // EdgeToEdge 설정 (One UI 7+ 대응)
setupEdgeToEdge()
// 2분 간격으로 폴링 시작
workerScheduler.schedulePeriodicPolling(2) workerScheduler.schedulePeriodicPolling(2)
setContent { setContent {
@@ -121,6 +129,61 @@ class MainActivity : ComponentActivity() {
} }
} }
/**
* EdgeToEdge 설정 - One UI 7+ 대응
*/
private fun setupEdgeToEdge() {
// WindowCompat을 사용한 설정
WindowCompat.setDecorFitsSystemWindows(window, false)
// statusBarColor와 navigationBarColor를 투명하게
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
// One UI 7+에서 시스템 바가 콘텐츠를 가리지 않도록 설정
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
// 시스템 바 인셋 처리
ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
} else {
@Suppress("DEPRECATION")
window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)
}
// 시스템 바 아이콘 색상 설정 (라이트/다크 모드)
val decorView = window.decorView
@Suppress("DEPRECATION")
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
window.insetsController?.let { controller ->
// 상태바 아이콘 밝게 (다크 모드용)
controller.setSystemBarsAppearance(
0,
android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
)
// 네비게이션바 아이콘 밝게 (다크 모드용)
controller.setSystemBarsAppearance(
0,
android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
)
}
} else {
@Suppress("DEPRECATION")
decorView.systemUiVisibility = decorView.systemUiVisibility or
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// 리시버 해제 // 리시버 해제

View File

@@ -5,21 +5,25 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch
import com.hotdeal.alarm.presentation.components.PermissionDialog import com.hotdeal.alarm.presentation.components.PermissionDialog
import com.hotdeal.alarm.presentation.deallist.DealListScreen import com.hotdeal.alarm.presentation.deallist.DealListScreen
import com.hotdeal.alarm.presentation.settings.SettingsScreen import com.hotdeal.alarm.presentation.settings.SettingsScreen
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MainScreen( fun MainScreen(
viewModel: MainViewModel, viewModel: MainViewModel,
@@ -49,21 +53,43 @@ fun MainScreen(
} }
} }
// TopAppBar 제거 - 화면을 넓게 사용 val pagerState = rememberPagerState(
NavHost( initialPage = 0,
navController = navController, pageCount = { 2 }
startDestination = Screen.DealList.route )
val coroutineScope = rememberCoroutineScope()
var currentPage by remember { mutableStateOf(0) }
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
currentPage = page
}
}
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing) // 시스템 바 패딩 적용
) { ) {
composable(Screen.DealList.route) { HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f)
) { page ->
when (page) {
0 -> {
DealListScreen( DealListScreen(
viewModel = viewModel, viewModel = viewModel,
onNavigateToSettings = { navController.navigate(Screen.Settings.route) } onNavigateToSettings = {
coroutineScope.launch { pagerState.animateScrollToPage(1) }
}
) )
} }
composable(Screen.Settings.route) { 1 -> {
SettingsScreen(viewModel = viewModel) SettingsScreen(viewModel = viewModel)
} }
} }
}
}
if (showPermissionDialog) { if (showPermissionDialog) {
PermissionDialog( PermissionDialog(

View File

@@ -1,22 +1,27 @@
package com.hotdeal.alarm.presentation.settings package com.hotdeal.alarm.presentation.settings
import android.Manifest import android.Manifest
import android.app.DownloadManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -26,14 +31,11 @@ import androidx.compose.material3.*
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.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.hotdeal.alarm.domain.model.Keyword import com.hotdeal.alarm.domain.model.Keyword
@@ -47,7 +49,7 @@ import com.hotdeal.alarm.util.PermissionHelper
import com.hotdeal.alarm.util.VersionManager import com.hotdeal.alarm.util.VersionManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(viewModel: MainViewModel) { fun SettingsScreen(viewModel: MainViewModel) {
val context = LocalContext.current val context = LocalContext.current
@@ -55,6 +57,10 @@ fun SettingsScreen(viewModel: MainViewModel) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentPollingInterval by viewModel.pollingInterval.collectAsState() val currentPollingInterval by viewModel.pollingInterval.collectAsState()
// 탭 상태
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 })
val tabTitles = listOf("알림", "사이트", "기타")
var showPermissionDialog by remember { mutableStateOf(false) } var showPermissionDialog by remember { mutableStateOf(false) }
var permissionDialogTitle by remember { mutableStateOf("") } var permissionDialogTitle by remember { mutableStateOf("") }
var permissionDialogMessage by remember { mutableStateOf("") } var permissionDialogMessage by remember { mutableStateOf("") }
@@ -74,6 +80,74 @@ fun SettingsScreen(viewModel: MainViewModel) {
val permissionStatus = PermissionHelper.checkAllPermissions(context) val permissionStatus = PermissionHelper.checkAllPermissions(context)
val hasEnabledSites = (uiState as? MainUiState.Success)?.siteConfigs?.any { it.isEnabled } ?: false val hasEnabledSites = (uiState as? MainUiState.Success)?.siteConfigs?.any { it.isEnabled } ?: false
Column(modifier = Modifier.fillMaxSize()) {
// 탭 레이블
PrimaryTabRow(
selectedTabIndex = pagerState.currentPage,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary
) {
tabTitles.forEachIndexed { index, title ->
Tab(
selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
text = {
Text(
text = title,
fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal
)
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// 탭 내용 (스와이프 가능)
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
beyondBoundsPageCount = 1
) { page ->
when (page) {
0 -> NotificationTab(
viewModel = viewModel,
permissionStatus = permissionStatus,
hasEnabledSites = hasEnabledSites,
currentPollingInterval = currentPollingInterval,
onRequestPermission = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
)
1 -> SitesTab(viewModel = viewModel)
2 -> MoreTab(viewModel = viewModel)
}
}
}
if (showPermissionDialog) {
PermissionDialog(
title = permissionDialogTitle,
message = permissionDialogMessage,
onDismiss = { showPermissionDialog = false },
onOpenSettings = { showPermissionDialog = false; permissionDialogAction() }
)
}
}
@Composable
private fun NotificationTab(
viewModel: MainViewModel,
permissionStatus: PermissionHelper.PermissionStatus,
hasEnabledSites: Boolean,
currentPollingInterval: Int,
onRequestPermission: () -> Unit
) {
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -81,22 +155,12 @@ fun SettingsScreen(viewModel: MainViewModel) {
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp) contentPadding = PaddingValues(vertical = 16.dp)
) { ) {
// 상단 알림 설정 섹션 // 알림 권한 설정
item { item {
NotificationSettingsHeader( NotificationSettingsHeader(
permissionStatus = permissionStatus, permissionStatus = permissionStatus,
hasEnabledSites = hasEnabledSites, hasEnabledSites = hasEnabledSites,
onRequestPermission = { onRequestPermission = 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)
}
) )
} }
@@ -112,7 +176,48 @@ fun SettingsScreen(viewModel: MainViewModel) {
) )
} }
// 사이트 설정 섹션 // 키워드 설정
item {
SectionHeader(
title = "키워드 알림",
icon = Icons.Outlined.NotificationsActive,
description = "키워드를 등록하면 해당 키워드가 포함된 핫딜 알림"
)
}
item {
KeywordInputCard(onAdd = { keyword -> viewModel.addKeyword(keyword) })
}
if (uiState is MainUiState.Success) {
items((uiState as MainUiState.Success).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(32.dp))
}
}
}
// ==================== 사이트 탭 ====================
@Composable
private fun SitesTab(viewModel: MainViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp)
) {
item { item {
SectionHeader( SectionHeader(
title = "사이트 선택", title = "사이트 선택",
@@ -132,51 +237,6 @@ fun SettingsScreen(viewModel: MainViewModel) {
) )
} }
} }
// 키워드 설정 섹션
item {
Spacer(modifier = Modifier.height(8.dp))
SectionHeader(
title = "키워드 알림",
icon = Icons.Outlined.NotificationsActive,
description = "키워드를 등록하면 해당 키워드가 포함된 핫딜 알림"
)
}
item {
KeywordInputCard(
onAdd = { keyword ->
viewModel.addKeyword(keyword)
}
)
}
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(8.dp))
VersionCard(
currentVersion = VersionManager.getCurrentVersion(context),
onCheckUpdate = {
scope.launch {
val currentCode = VersionManager.getCurrentVersionCode(context)
val remoteInfo = VersionManager.checkForUpdate()
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) {
Toast.makeText(context, "새 버전 ${remoteInfo.version} 사용 가능", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show()
}
}
}
)
}
} }
else -> { else -> {
item { item {
@@ -191,24 +251,252 @@ fun SettingsScreen(viewModel: MainViewModel) {
} }
} }
} }
// 하단 여백
item {
Spacer(modifier = Modifier.height(32.dp))
}
}
}
// ==================== 기타 탭 ====================
@Composable
private fun MoreTab(viewModel: MainViewModel) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showDeleteDialog by remember { mutableStateOf(false) }
var isCheckingUpdate by remember { mutableStateOf(false) }
val toastEvent by viewModel.toastEvent.collectAsStateWithLifecycle(initialValue = null)
LaunchedEffect(toastEvent) {
toastEvent?.let { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
} }
if (showPermissionDialog) { LazyColumn(
PermissionDialog( modifier = Modifier
title = permissionDialogTitle, .fillMaxSize()
message = permissionDialogMessage, .padding(horizontal = 16.dp),
onDismiss = { showPermissionDialog = false }, verticalArrangement = Arrangement.spacedBy(12.dp),
onOpenSettings = { showPermissionDialog = false; permissionDialogAction() } contentPadding = PaddingValues(vertical = 16.dp)
) {
// 데이터 관리
item {
SectionHeader(
title = "데이터 관리",
icon = Icons.Outlined.Storage,
description = "저장된 데이터를 관리합니다"
)
}
item {
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.errorContainer,
CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.DeleteSweep,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "파싱 데이터 삭제",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "저장된 모든 핫딜 데이터를 삭제합니다",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
FilledTonalIconButton(
onClick = { showDeleteDialog = true },
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = "삭제",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
// 앱 정보
item {
Spacer(modifier = Modifier.height(8.dp))
SectionHeader(
title = "앱 정보",
icon = Icons.Outlined.Info,
description = "앱 버전 및 업데이트"
)
}
item {
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${VersionManager.getCurrentVersion(context)}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Button(
onClick = {
scope.launch {
isCheckingUpdate = true
val currentCode = VersionManager.getCurrentVersionCode(context)
val remoteInfo = VersionManager.checkForUpdate()
isCheckingUpdate = false
if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) {
downloadAndInstallApk(context, remoteInfo)
} else {
Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show()
}
}
},
enabled = !isCheckingUpdate,
shape = RoundedCornerShape(12.dp)
) {
if (isCheckingUpdate) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = Icons.Outlined.SystemUpdate,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
Spacer(modifier = Modifier.width(6.dp))
Text(if (isCheckingUpdate) "확인 중..." else "업데이트 확인")
}
}
}
}
// 하단 여백
item {
Spacer(modifier = Modifier.height(32.dp))
}
}
// 삭제 확인 다이얼로그
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("데이터 삭제") },
text = { Text("저장된 모든 핫딜 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.") },
confirmButton = {
Button(
onClick = {
viewModel.deleteAllParsedData()
showDeleteDialog = false
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("삭제")
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("취소")
}
}
) )
} }
} }
private fun downloadAndInstallApk(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(updateInfo.updateUrl))
.setTitle("핫딜 알람 업데이트")
.setDescription("버전 ${updateInfo.version} 다운로드 중...")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "hotdeal_alarm_${updateInfo.version}.apk")
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
downloadManager.enqueue(request)
Toast.makeText(context, "다운로드가 시작되었습니다", Toast.LENGTH_SHORT).show()
}
// ==================== 공통 컴포넌트 ====================
@Composable @Composable
private fun NotificationSettingsHeader( private fun NotificationSettingsHeader(
permissionStatus: PermissionHelper.PermissionStatus, permissionStatus: PermissionHelper.PermissionStatus,
hasEnabledSites: Boolean, hasEnabledSites: Boolean,
onRequestPermission: () -> Unit, onRequestPermission: () -> Unit
onOpenSystemSettings: () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -220,10 +508,7 @@ private fun NotificationSettingsHeader(
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(modifier = Modifier.padding(16.dp)) {
modifier = Modifier.padding(16.dp)
) {
// 헤더
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -261,7 +546,6 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 알림 권한 상태
PermissionStatusRow( PermissionStatusRow(
icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
title = "알림 권한", title = "알림 권한",
@@ -275,7 +559,6 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 정확한 알람 (리마인더) 권한 상태
PermissionStatusRow( PermissionStatusRow(
icon = if (permissionStatus.hasExactAlarmPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, icon = if (permissionStatus.hasExactAlarmPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning,
title = "리마인더 및 알람", title = "리마인더 및 알람",
@@ -289,7 +572,6 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 알 수 없는 앱 설치 권한 상태
PermissionStatusRow( PermissionStatusRow(
icon = if (permissionStatus.canInstallUnknownApps) Icons.Filled.CheckCircle else Icons.Filled.Warning, icon = if (permissionStatus.canInstallUnknownApps) Icons.Filled.CheckCircle else Icons.Filled.Warning,
title = "앱 설치 권한", title = "앱 설치 권한",
@@ -303,7 +585,6 @@ private fun NotificationSettingsHeader(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 사이트 선택 상태
PermissionStatusRow( PermissionStatusRow(
icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error, icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error,
title = "사이트 선택", title = "사이트 선택",
@@ -311,7 +592,6 @@ private fun NotificationSettingsHeader(
isOk = hasEnabledSites isOk = hasEnabledSites
) )
// 시스템 설정 버튼
if (permissionStatus.hasNotificationPermission || permissionStatus.hasExactAlarmPermission || permissionStatus.canInstallUnknownApps) { if (permissionStatus.hasNotificationPermission || permissionStatus.hasExactAlarmPermission || permissionStatus.canInstallUnknownApps) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OutlinedButton( OutlinedButton(
@@ -371,6 +651,7 @@ private fun PermissionStatusRow(
} }
} }
} }
@Composable @Composable
private fun SectionHeader( private fun SectionHeader(
title: String, title: String,
@@ -420,12 +701,8 @@ private fun PollingIntervalCard(
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(modifier = Modifier.padding(20.dp)) {
modifier = Modifier.padding(20.dp) Row(verticalAlignment = Alignment.CenterVertically) {
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(44.dp) .size(44.dp)
@@ -459,7 +736,6 @@ private fun PollingIntervalCard(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 선택 옵션들
val options = listOf( val options = listOf(
Triple(1, "1분", "빠름"), Triple(1, "1분", "빠름"),
Triple(2, "2분", "권장"), Triple(2, "2분", "권장"),
@@ -524,10 +800,6 @@ private fun PollingOptionChip(
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
else else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
border = if (isSelected)
null
else
null,
interactionSource = interactionSource interactionSource = interactionSource
) { ) {
Column( Column(
@@ -575,15 +847,11 @@ private fun EnhancedSiteCard(
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) { ) {
Column( Column(modifier = Modifier.padding(16.dp)) {
modifier = Modifier.padding(16.dp)
) {
// 사이트 헤더
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
// 사이트 색상 인디케이터
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@@ -612,7 +880,6 @@ private fun EnhancedSiteCard(
) )
} }
// 진행률 표시
if (totalCount > 0) { if (totalCount > 0) {
Text( Text(
text = "${(enabledCount * 100 / totalCount)}%", text = "${(enabledCount * 100 / totalCount)}%",
@@ -623,7 +890,6 @@ private fun EnhancedSiteCard(
} }
} }
// 게시판 목록
if (configs.isNotEmpty()) { if (configs.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
configs.forEach { config -> configs.forEach { config ->
@@ -654,11 +920,8 @@ private fun EnhancedSiteCard(
} }
@Composable @Composable
private fun KeywordInputCard( private fun KeywordInputCard(onAdd: (String) -> Unit) {
onAdd: (String) -> Unit
) {
var keywordText by remember { mutableStateOf("") } var keywordText by remember { mutableStateOf("") }
var isExpanded by remember { mutableStateOf(false) }
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -667,9 +930,7 @@ private fun KeywordInputCard(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
) )
) { ) {
Column( Column(modifier = Modifier.padding(16.dp)) {
modifier = Modifier.padding(16.dp)
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -713,7 +974,6 @@ private fun KeywordInputCard(
} }
} }
// 힌트
AnimatedVisibility( AnimatedVisibility(
visible = keywordText.isEmpty(), visible = keywordText.isEmpty(),
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
@@ -743,7 +1003,7 @@ private fun EnhancedKeywordCard(
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = CardDefaults.elevatedCardColors( colors = CardDefaults.elevatedCardColors(
containerColor = if (isEnabled) containerColor = if (isEnabled)
Color(0xFFFFEBEE) // 옅은 빨간색 배경 (Material Red 50) Color(0xFFFFEBEE)
else else
MaterialTheme.colorScheme.surface MaterialTheme.colorScheme.surface
), ),
@@ -757,11 +1017,10 @@ private fun EnhancedKeywordCard(
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 키워드 아이콘
Surface( Surface(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
color = if (isEnabled) color = if (isEnabled)
Color(0xFFE53935).copy(alpha = 0.12f) // 빨간색 Color(0xFFE53935).copy(alpha = 0.12f)
else else
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.height(36.dp) modifier = Modifier.height(36.dp)
@@ -790,7 +1049,6 @@ private fun EnhancedKeywordCard(
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
// 키워드 텍스트
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = keyword.keyword, text = keyword.keyword,
@@ -802,17 +1060,13 @@ private fun EnhancedKeywordCard(
text = if (isEnabled) "알림 활성화" else "알림 비활성화", text = if (isEnabled) "알림 활성화" else "알림 비활성화",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = if (isEnabled) color = if (isEnabled)
Color(0xFFE53935) // 빨간색 Color(0xFFE53935)
else else
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
// 액션 버튼들 Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
// 토글 버튼
IconButton( IconButton(
onClick = onToggle, onClick = onToggle,
modifier = Modifier.size(36.dp) modifier = Modifier.size(36.dp)
@@ -821,14 +1075,13 @@ private fun EnhancedKeywordCard(
imageVector = if (isEnabled) Icons.Filled.Notifications else Icons.Outlined.NotificationsNone, imageVector = if (isEnabled) Icons.Filled.Notifications else Icons.Outlined.NotificationsNone,
contentDescription = if (isEnabled) "알림 끄기" else "알림 켜기", contentDescription = if (isEnabled) "알림 끄기" else "알림 켜기",
tint = if (isEnabled) tint = if (isEnabled)
Color(0xFFE53935) // 빨간색 Color(0xFFE53935)
else else
MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
// 삭제 버튼
IconButton( IconButton(
onClick = onDelete, onClick = onDelete,
modifier = Modifier.size(36.dp) modifier = Modifier.size(36.dp)
@@ -844,70 +1097,3 @@ private fun EnhancedKeywordCard(
} }
} }
} }
@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("업데이트 확인")
}
}
}
}