From da4ad8d3a336ba9f03c8ae6b662cfea2018c4852 Mon Sep 17 00:00:00 2001 From: sanjeok77 <1+sanjeok77@noreply.localhost> Date: Thu, 20 Aug 2026 23:28:59 +0000 Subject: [PATCH] v0.2.4: One UI 9 & Material 3 Expressive UI/UX Redesign --- .../presentation/settings/SettingsScreen.kt | 1394 +++++++---------- 1 file changed, 593 insertions(+), 801 deletions(-) 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 67c12a7..4ccb9cd 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,13 +1,8 @@ package com.hotdeal.alarm.presentation.settings import android.Manifest -import android.app.DownloadManager import android.content.Context -import android.content.Intent -import android.net.Uri import android.os.Build -import android.os.Environment -import android.provider.Settings import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -15,8 +10,8 @@ import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -31,25 +26,31 @@ 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.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hotdeal.alarm.domain.model.Keyword import com.hotdeal.alarm.domain.model.SiteConfig import com.hotdeal.alarm.domain.model.SiteType -import com.hotdeal.alarm.presentation.components.PermissionDialog +import com.hotdeal.alarm.presentation.components.* 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.ui.theme.* import com.hotdeal.alarm.util.ApkDownloadManager import com.hotdeal.alarm.util.PermissionHelper import com.hotdeal.alarm.util.VersionManager import kotlinx.coroutines.launch +/** + * One UI 9 & Material 3 Expressive Bento Settings Screen + * 인체공학적 세그먼트 Pill 탭 및 모듈형 Bento 그리드 구조의 프리미엄 설정 화면 + */ @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(viewModel: MainViewModel) { @@ -58,9 +59,8 @@ fun SettingsScreen(viewModel: MainViewModel) { val scope = rememberCoroutineScope() val currentPollingInterval by viewModel.pollingInterval.collectAsState() - // 탭 상태 val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) - val tabTitles = listOf("알림", "사이트", "기타") + val tabTitles = listOf("알림 & 키워드", "사이트 관리", "데이터 & 정보") var showPermissionDialog by remember { mutableStateOf(false) } var permissionDialogTitle by remember { mutableStateOf("") } @@ -81,30 +81,62 @@ fun SettingsScreen(viewModel: MainViewModel) { val permissionStatus = PermissionHelper.checkAllPermissions(context) 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 + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + // ============================================ + // 1. One UI 9 Segmented Pill Tab Bar + // ============================================ + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = CornerRadius.shapePill, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = androidx.compose.foundation.BorderStroke( + 0.8.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) + ) ) { - tabTitles.forEachIndexed { index, title -> - Tab( - selected = pagerState.currentPage == index, - onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, - text = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + tabTitles.forEachIndexed { index, title -> + val isSelected = pagerState.currentPage == index + val tabBgColor = if (isSelected) MaterialTheme.colorScheme.surface else Color.Transparent + val tabTextColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant + + Box( + modifier = Modifier + .weight(1f) + .height(38.dp) + .clip(CornerRadius.shapePill) + .background(tabBgColor) + .elasticPressClickable { + scope.launch { pagerState.animateScrollToPage(index) } + }, + contentAlignment = Alignment.Center + ) { Text( text = title, - fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium + ), + color = tabTextColor ) - }, - selectedContentColor = MaterialTheme.colorScheme.primary, - unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) + } + } } } - // 탭 내용 (스와이프 가능) + // ============================================ + // 2. 탭 컨텐츠 (Horizontal Pager) + // ============================================ HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), @@ -138,6 +170,9 @@ fun SettingsScreen(viewModel: MainViewModel) { } } +// ============================================ +// 탭 1: 알림 & 키워드 (NotificationTab) +// ============================================ @Composable private fun NotificationTab( viewModel: MainViewModel, @@ -148,66 +183,263 @@ private fun NotificationTab( ) { val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var keywordInput by remember { mutableStateOf("") } + + val activeKeywords = (uiState as? MainUiState.Success)?.keywords ?: emptyList() + val activeSitesCount = (uiState as? MainUiState.Success)?.siteConfigs?.count { it.isEnabled } ?: 0 LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 16.dp) + verticalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) ) { - // 알림 권한 설정 + // One UI 9 Now Status Bento 2x2 카드 item { - NotificationSettingsHeader( - permissionStatus = permissionStatus, - hasEnabledSites = hasEnabledSites, - onRequestPermission = onRequestPermission - ) - } + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Dashboard, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Text( + text = "실시간 모니터링 요약", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } - // 폴링 주기 설정 - item { - PollingIntervalCard( - currentInterval = currentPollingInterval, - onIntervalChange = { minutes -> - viewModel.stopPolling() - viewModel.startPolling(minutes) - Toast.makeText(context, "${minutes}분으로 변경됨", Toast.LENGTH_SHORT).show() + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + BentoMiniTile( + title = "알림 상태", + value = if (permissionStatus.hasNotificationPermission) "정상 가동" else "권한 필요", + isHighlight = !permissionStatus.hasNotificationPermission, + modifier = Modifier.weight(1f), + onClick = if (!permissionStatus.hasNotificationPermission) onRequestPermission else null + ) + BentoMiniTile( + title = "감시 주기", + value = "${currentPollingInterval}분 간격", + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + BentoMiniTile( + title = "활성 사이트", + value = "${activeSitesCount}개 선택됨", + modifier = Modifier.weight(1f) + ) + BentoMiniTile( + title = "등록 키워드", + value = "${activeKeywords.size}개 등록됨", + modifier = Modifier.weight(1f) + ) + } } - ) + } } - // 키워드 설정 + // 모듈형 수집 주기 선택 블록 item { - SectionHeader( - title = "키워드 알림", - icon = Icons.Outlined.NotificationsActive, - description = "키워드를 등록하면 해당 키워드가 포함된 핫딜 알림" - ) + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Text( + text = "수집 주기 설정", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "백그라운드에서 새로운 핫딜을 확인하는 주기입니다.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 1분 / 2분 / 5분 / 10분 / 30분 Pill 선택기 + val intervals = listOf(1, 2, 5, 10, 30) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + intervals.forEach { minutes -> + val isSelected = currentPollingInterval == minutes + val bgColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant + + Box( + modifier = Modifier + .weight(1f) + .height(38.dp) + .clip(CornerRadius.shapeSmall) + .background(bgColor) + .elasticPressClickable { + viewModel.stopPolling() + viewModel.startPolling(minutes.toLong()) + Toast.makeText(context, "${minutes}분 주기로 변경되었습니다", Toast.LENGTH_SHORT).show() + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "${minutes}분", + style = MaterialTheme.typography.labelMedium.copy( + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium + ), + color = textColor + ) + } + } + } + } + } } + // 키워드 알림 관리 섹션 item { - KeywordInputCard(onAdd = { keyword -> viewModel.addKeyword(keyword) }) + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.NotificationsActive, + contentDescription = null, + tint = KeywordGold, + modifier = Modifier.size(18.dp) + ) + Text( + text = "키워드 등록 & 관리", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "등록한 키워드가 포함된 핫딜이 발견되면 즉시 푸시 알림을 발송합니다.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(14.dp)) + + // 키워드 입력 필드 + 추가 버튼 일체형 + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = keywordInput, + onValueChange = { keywordInput = it }, + placeholder = { + Text( + text = "키워드 입력 (예: 모니터, 칫솔, 그래픽카드)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + }, + singleLine = true, + shape = CornerRadius.shapeNormal, + modifier = Modifier.weight(1f), + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f) + ) + ) + + Button( + onClick = { + if (keywordInput.isNotBlank()) { + viewModel.addKeyword(keywordInput.trim()) + keywordInput = "" + } + }, + shape = CornerRadius.shapeNormal, + modifier = Modifier.height(52.dp), + enabled = keywordInput.isNotBlank() + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "추가", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = "추가", fontWeight = FontWeight.Bold) + } + } + } + } } - if (uiState is MainUiState.Success) { - items((uiState as MainUiState.Success).keywords, key = { it.id }) { keyword -> - EnhancedKeywordCard( + // 등록된 키워드 목록 + if (activeKeywords.isNotEmpty()) { + item { + Text( + text = "등록된 키워드 (${activeKeywords.size}개)", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) + ) + } + + items(activeKeywords, key = { it.id }) { keyword -> + OneUIKeywordTile( keyword = keyword, onToggle = { viewModel.toggleKeyword(keyword.id, !keyword.isEnabled) }, onDelete = { viewModel.deleteKeyword(keyword.id) } ) } } - - // 하단 여백 - item { - Spacer(modifier = Modifier.height(32.dp)) - } } } -// ==================== 사이트 탭 ==================== +// ============================================ +// 탭 2: 사이트 관리 (SitesTab) +// ============================================ @Composable private fun SitesTab(viewModel: MainViewModel) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -216,24 +448,46 @@ private fun SitesTab(viewModel: MainViewModel) { modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 16.dp) + verticalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) ) { item { - SectionHeader( - title = "사이트 선택", - icon = Icons.Outlined.Language, - description = "모니터링할 사이트를 선택하세요" - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Language, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "수집 대상 커뮤니티", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Text( + text = "원하는 사이트 및 세부 게시판을 켜고 끌 수 있습니다.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } } when (val state = uiState) { is MainUiState.Success -> { SiteType.entries.forEach { site -> - item { - EnhancedSiteCard( + val configs = state.siteConfigs.filter { it.siteName == site.name } + item(key = site.name) { + OneUISiteBentoCard( siteType = site, - configs = state.siteConfigs.filter { it.siteName == site.name }, + configs = configs, onToggle = { key, enabled -> viewModel.toggleSiteConfig(key, enabled) } ) } @@ -252,51 +506,25 @@ private fun SitesTab(viewModel: MainViewModel) { } } } - - // 하단 여백 - item { - Spacer(modifier = Modifier.height(32.dp)) - } } } -// ==================== 기타 탭 ==================== +// ============================================ +// 탭 3: 데이터 & 정보 (MoreTab) +// ============================================ @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) } - var downloadId by remember { mutableStateOf(null) } var isDownloading 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() - } - } - - DisposableEffect(downloadId) { - if (downloadId != null) { - val receiver = ApkDownloadManager.registerDownloadCompleteReceiver( - context = context, - downloadId = downloadId!!, - onComplete = { - isDownloading = false - ApkDownloadManager.installApk(context) - }, - onFailed = { - isDownloading = false - Toast.makeText(context, "다운로드 실패. 다시 시도해주세요.", Toast.LENGTH_SHORT).show() - } - ) - onDispose { - ApkDownloadManager.unregisterDownloadCompleteReceiver(context) - } - } else { - onDispose { } + toastEvent?.let { msg -> + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() } } @@ -304,132 +532,54 @@ private fun MoreTab(viewModel: MainViewModel) { modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 16.dp) + verticalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) ) { - // 데이터 관리 + // One UI 9 App Hero Info Card 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) + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() ) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier - .size(44.dp) - .background( - MaterialTheme.colorScheme.errorContainer, - CircleShape - ), + .size(64.dp) + .clip(CornerRadius.shapeSquircle) + .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Filled.DeleteSweep, + imageVector = Icons.Filled.LocalFireDepartment, contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.size(24.dp) + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.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 = "앱 버전 및 업데이트" - ) - } + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "핫딜 알람 (HotDeal Alarm)", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "v${VersionManager.getCurrentVersion(context)} • One UI 9 Edition", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(16.dp)) - 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 { @@ -439,67 +589,98 @@ private fun MoreTab(viewModel: MainViewModel) { isCheckingUpdate = false if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) { - downloadId = startDownload(context, remoteInfo) - isDownloading = true + Toast.makeText(context, "새 버전(${remoteInfo.version})이 있습니다!", Toast.LENGTH_LONG).show() } else { - Toast.makeText(context, "최신 버전입니다", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "최신 버전을 사용 중입니다", Toast.LENGTH_SHORT).show() } } }, - enabled = !isCheckingUpdate && !isDownloading, - shape = RoundedCornerShape(12.dp) + shape = CornerRadius.shapeNormal, + modifier = Modifier.height(44.dp), + enabled = !isCheckingUpdate && !isDownloading ) { - if (isCheckingUpdate) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp - ) - } else if (isDownloading) { - 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)) + Icon( + imageVector = Icons.Outlined.SystemUpdate, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) Text( - when { - isCheckingUpdate -> "확인 중..." - isDownloading -> "다운로드 중..." - else -> "업데이트 확인" - } + text = if (isCheckingUpdate) "확인 중..." else "업데이트 확인", + fontWeight = FontWeight.SemiBold ) } } } } - // 하단 여백 + // 데이터 관리 Bento 카드 item { - Spacer(modifier = Modifier.height(32.dp)) + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Storage, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + Text( + text = "데이터 및 캐시 관리", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "수집되어 로컬 DB에 캐싱된 모든 핫딜 데이터를 일괄 삭제합니다.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(14.dp)) + + FilledTonalButton( + onClick = { showDeleteDialog = true }, + shape = CornerRadius.shapeNormal, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ), + modifier = Modifier.fillMaxWidth().height(48.dp) + ) { + Icon( + imageVector = Icons.Outlined.DeleteSweep, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = "수집 데이터 전체 비우기", fontWeight = FontWeight.Bold) + } + } + } } } - // 삭제 확인 다이얼로그 if (showDeleteDialog) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, - title = { Text("데이터 삭제") }, - text = { Text("저장된 모든 핫딜 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.") }, + title = { Text("수집 데이터 삭제") }, + text = { Text("저장된 모든 핫딜 데이터를 삭제하시겠습니까? (설정 및 키워드는 유지됩니다)") }, confirmButton = { Button( onClick = { viewModel.deleteAllParsedData() showDeleteDialog = false }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) ) { Text("삭제") } @@ -513,383 +694,142 @@ private fun MoreTab(viewModel: MainViewModel) { } } - private fun startDownload(context: Context, updateInfo: com.hotdeal.alarm.util.UpdateInfo): Long { - return ApkDownloadManager.downloadApk(context, updateInfo) - } - -// ==================== 공통 컴포넌트 ==================== +// ============================================ +// 서브 컴포넌트들 (One UI 9 UI Tiles) +// ============================================ @Composable -private fun NotificationSettingsHeader( - permissionStatus: PermissionHelper.PermissionStatus, - hasEnabledSites: Boolean, - onRequestPermission: () -> Unit -) { - val context = LocalContext.current - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .size(40.dp) - .background( - MaterialTheme.colorScheme.primaryContainer, - CircleShape - ), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Filled.Notifications, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(22.dp) - ) - } - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = "알림 설정", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Text( - text = "핫딜 알림을 받기 위한 설정", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - PermissionStatusRow( - icon = if (permissionStatus.hasNotificationPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, - title = "알림 권한", - description = if (permissionStatus.hasNotificationPermission) "허용됨" else "필요함", - isOk = permissionStatus.hasNotificationPermission, - onAction = if (!permissionStatus.hasNotificationPermission) { - { onRequestPermission() } - } else null, - actionLabel = "허용" - ) - - Spacer(modifier = Modifier.height(8.dp)) - - PermissionStatusRow( - icon = if (permissionStatus.hasExactAlarmPermission) Icons.Filled.CheckCircle else Icons.Filled.Warning, - title = "리마인더 및 알람", - description = if (permissionStatus.hasExactAlarmPermission) "허용됨" else "정확한 시간 알림에 필요", - isOk = permissionStatus.hasExactAlarmPermission, - onAction = if (!permissionStatus.hasExactAlarmPermission) { - { PermissionHelper.openExactAlarmSettings(context) } - } else null, - actionLabel = "설정" - ) - - Spacer(modifier = Modifier.height(8.dp)) - - PermissionStatusRow( - icon = if (permissionStatus.canInstallUnknownApps) Icons.Filled.CheckCircle else Icons.Filled.Warning, - title = "앱 설치 권한", - description = if (permissionStatus.canInstallUnknownApps) "허용됨" else "업데이트 설치에 필요", - isOk = permissionStatus.canInstallUnknownApps, - onAction = if (!permissionStatus.canInstallUnknownApps) { - { PermissionHelper.openUnknownAppsSettings(context) } - } else null, - actionLabel = "설정" - ) - - Spacer(modifier = Modifier.height(8.dp)) - - PermissionStatusRow( - icon = if (hasEnabledSites) Icons.Filled.CheckCircle else Icons.Filled.Error, - title = "사이트 선택", - description = if (hasEnabledSites) "완료" else "최소 1개 이상 필요", - isOk = hasEnabledSites - ) - - if (permissionStatus.hasNotificationPermission || permissionStatus.hasExactAlarmPermission || permissionStatus.canInstallUnknownApps) { - Spacer(modifier = Modifier.height(12.dp)) - OutlinedButton( - onClick = { PermissionHelper.openAppSettings(context) }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("시스템 설정 열기") - } - } - } - } -} - -@Composable -private fun PermissionStatusRow( - icon: ImageVector, +private fun BentoMiniTile( title: String, - description: String, - isOk: Boolean, - onAction: (() -> Unit)? = null, - actionLabel: String = "" + value: String, + isHighlight: Boolean = false, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (isOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (onAction != null) { - TextButton(onClick = onAction) { - Text(actionLabel, style = MaterialTheme.typography.labelMedium) - } - } + val bgColor = if (isHighlight) { + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) } -} -@Composable -private fun SectionHeader( - title: String, - icon: ImageVector, - description: String -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) + val textColor = if (isHighlight) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } + + Box( + modifier = modifier + .clip(CornerRadius.shapeNormal) + .background(bgColor) + .then(if (onClick != null) Modifier.elasticPressClickable(onClick = onClick) else Modifier) + .padding(horizontal = 12.dp, vertical = 10.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( - Triple(1, "1분", "빠름"), - Triple(2, "2분", "권장"), - Triple(5, "5분", "보통"), - Triple(10, "10분", "느림"), - Triple(15, "15분", "매우 느림"), - Triple(30, "30분", "절전") - ) - - 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), - 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) + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = value, + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold), + color = textColor ) } } } @Composable -private fun EnhancedSiteCard( +private fun OneUIKeywordTile( + keyword: Keyword, + onToggle: () -> Unit, + onDelete: () -> Unit +) { + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(KeywordGold, CircleShape) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = keyword.keyword, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f) + ) + + Switch( + checked = keyword.isEnabled, + onCheckedChange = { onToggle() }, + modifier = Modifier.scale(0.85f) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + IconButton( + onClick = onDelete, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "삭제", + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(18.dp) + ) + } + } + } +} + +@Composable +private fun OneUISiteBentoCard( siteType: SiteType, configs: List, onToggle: (String, Boolean) -> Unit ) { val siteColor = getSiteColor(siteType) - val enabledCount = configs.count { it.isEnabled } - val totalCount = configs.size + val isAnyEnabled = configs.any { it.isEnabled } + var isExpanded by remember { mutableStateOf(false) } - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + SquircleCard( + containerColor = MaterialTheme.colorScheme.surface, + borderColor = if (isAnyEnabled) siteColor.copy(alpha = 0.4f) else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + borderWidth = if (isAnyEnabled) 1.2.dp else 0.8.dp, + modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.padding(16.dp)) { + // 상단 마스터 행 Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { - Box( - modifier = Modifier - .size(40.dp) - .background(siteColor.copy(alpha = 0.15f), CircleShape), - contentAlignment = Alignment.Center + // 브랜드 뱃지 + Surface( + shape = CornerRadius.shapeSmall, + color = siteColor.copy(alpha = 0.14f), + modifier = Modifier.size(36.dp) ) { - Box( - modifier = Modifier - .size(20.dp) - .background(siteColor, CircleShape) - ) + Box(contentAlignment = Alignment.Center) { + Text( + text = siteType.displayName.take(1), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.ExtraBold), + color = siteColor + ) + } } Spacer(modifier = Modifier.width(12.dp)) @@ -897,228 +837,80 @@ private fun EnhancedSiteCard( Column(modifier = Modifier.weight(1f)) { Text( text = siteType.displayName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface ) Text( - text = "$enabledCount / $totalCount 게시판 활성화", + text = "${configs.count { it.isEnabled }}/${configs.size}개 게시판 활성화", 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("") } - - 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 = "" + // 전체 온/오프 일괄 마스터 스위치 + Switch( + checked = isAnyEnabled, + onCheckedChange = { enableAll -> + configs.forEach { config -> + onToggle(config.siteBoardKey, enableAll) } }, - modifier = Modifier.size(56.dp), - shape = RoundedCornerShape(16.dp), - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = MaterialTheme.colorScheme.primary - ) + modifier = Modifier.scale(0.9f) + ) + + IconButton( + onClick = { isExpanded = !isExpanded }, + modifier = Modifier.size(32.dp) ) { Icon( - imageVector = Icons.Filled.Add, - contentDescription = "추가", - modifier = Modifier.size(24.dp) + imageVector = if (isExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, + contentDescription = "게시판 상세", + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } + // 아코디언 세부 게시판 목록 AnimatedVisibility( - visible = keywordText.isEmpty(), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() ) { - 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 isEnabled = keyword.isEnabled - - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.elevatedCardColors( - containerColor = if (isEnabled) - Color(0xFFFFEBEE) - else - MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.elevatedCardElevation( - defaultElevation = 2.dp - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Surface( - shape = RoundedCornerShape(10.dp), - color = if (isEnabled) - Color(0xFFE53935).copy(alpha = 0.12f) - else - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - modifier = Modifier.height(36.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier.padding(horizontal = 10.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) ) { - Box( - modifier = Modifier - .size(8.dp) - .background( - if (isEnabled) Color(0xFFE53935) else MaterialTheme.colorScheme.outline, - CircleShape + Divider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), + thickness = 0.8.dp + ) + Spacer(modifier = Modifier.height(8.dp)) + + siteType.boards.forEach { board -> + val config = configs.find { it.boardName == board.id } + val isEnabled = config?.isEnabled == true + val key = config?.siteBoardKey ?: "${siteType.name}_${board.id}" + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = board.displayName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) ) - ) - Text( - text = "키워드", - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, - color = if (isEnabled) Color(0xFFE53935) else MaterialTheme.colorScheme.outline - ) - } - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = keyword.keyword, - style = MaterialTheme.typography.titleMedium, - fontWeight = if (isEnabled) FontWeight.Bold else FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = if (isEnabled) "알림 활성화" else "알림 비활성화", - style = MaterialTheme.typography.bodySmall, - color = if (isEnabled) - Color(0xFFE53935) - else - MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { - IconButton( - onClick = onToggle, - modifier = Modifier.size(36.dp) - ) { - Icon( - imageVector = if (isEnabled) Icons.Filled.Notifications else Icons.Outlined.NotificationsNone, - contentDescription = if (isEnabled) "알림 끄기" else "알림 켜기", - tint = if (isEnabled) - Color(0xFFE53935) - else - MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) - ) - } - - IconButton( - onClick = onDelete, - modifier = Modifier.size(36.dp) - ) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "삭제", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(18.dp) - ) + Switch( + checked = isEnabled, + onCheckedChange = { onToggle(key, it) }, + modifier = Modifier.scale(0.8f) + ) + } + } } } }