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 4ccb9cd..0819060 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 @@ -14,11 +14,10 @@ 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 +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* @@ -29,7 +28,6 @@ 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 @@ -42,20 +40,19 @@ 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.* -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 그리드 구조의 프리미엄 설정 화면 + * 순서 변경(Reordering) 및 Bento 그리드 구조를 지원하는 프리미엄 설정 화면 */ @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(viewModel: MainViewModel) { val context = LocalContext.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsState() val scope = rememberCoroutineScope() val currentPollingInterval by viewModel.pollingInterval.collectAsState() @@ -79,7 +76,6 @@ fun SettingsScreen(viewModel: MainViewModel) { } val permissionStatus = PermissionHelper.checkAllPermissions(context) - val hasEnabledSites = (uiState as? MainUiState.Success)?.siteConfigs?.any { it.isEnabled } ?: false Column( modifier = Modifier @@ -92,7 +88,7 @@ fun SettingsScreen(viewModel: MainViewModel) { Surface( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(horizontal = 14.dp, vertical = 6.dp), shape = CornerRadius.shapePill, color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), border = androidx.compose.foundation.BorderStroke( @@ -103,8 +99,8 @@ fun SettingsScreen(viewModel: MainViewModel) { Row( modifier = Modifier .fillMaxWidth() - .padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) + .padding(3.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp) ) { tabTitles.forEachIndexed { index, title -> val isSelected = pagerState.currentPage == index @@ -114,7 +110,7 @@ fun SettingsScreen(viewModel: MainViewModel) { Box( modifier = Modifier .weight(1f) - .height(38.dp) + .height(36.dp) .clip(CornerRadius.shapePill) .background(tabBgColor) .elasticPressClickable { @@ -124,8 +120,9 @@ fun SettingsScreen(viewModel: MainViewModel) { ) { Text( text = title, - style = MaterialTheme.typography.labelMedium.copy( - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + fontSize = 12.sp ), color = tabTextColor ) @@ -146,7 +143,6 @@ fun SettingsScreen(viewModel: MainViewModel) { 0 -> NotificationTab( viewModel = viewModel, permissionStatus = permissionStatus, - hasEnabledSites = hasEnabledSites, currentPollingInterval = currentPollingInterval, onRequestPermission = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -177,12 +173,11 @@ fun SettingsScreen(viewModel: MainViewModel) { private fun NotificationTab( viewModel: MainViewModel, permissionStatus: PermissionHelper.PermissionStatus, - hasEnabledSites: Boolean, currentPollingInterval: Int, onRequestPermission: () -> Unit ) { val context = LocalContext.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsState() var keywordInput by remember { mutableStateOf("") } val activeKeywords = (uiState as? MainUiState.Success)?.keywords ?: emptyList() @@ -191,17 +186,17 @@ private fun NotificationTab( LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) + .padding(horizontal = 14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(top = 6.dp, bottom = 24.dp) ) { - // One UI 9 Now Status Bento 2x2 카드 + // Now Status Bento 2x2 카드 item { SquircleCard( containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(14.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -210,7 +205,7 @@ private fun NotificationTab( imageVector = Icons.Outlined.Dashboard, contentDescription = null, tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(17.dp) ) Text( text = "실시간 모니터링 요약", @@ -219,11 +214,11 @@ private fun NotificationTab( ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { BentoMiniTile( title = "알림 상태", @@ -239,11 +234,11 @@ private fun NotificationTab( ) } - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { BentoMiniTile( title = "활성 사이트", @@ -266,7 +261,7 @@ private fun NotificationTab( containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(14.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -275,7 +270,7 @@ private fun NotificationTab( imageVector = Icons.Outlined.Timer, contentDescription = null, tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(17.dp) ) Text( text = "수집 주기 설정", @@ -284,20 +279,19 @@ private fun NotificationTab( ) } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = "백그라운드에서 새로운 핫딜을 확인하는 주기입니다.", - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.5.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) - // 1분 / 2분 / 5분 / 10분 / 30분 Pill 선택기 val intervals = listOf(1, 2, 5, 10, 30) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp) + horizontalArrangement = Arrangement.spacedBy(5.dp) ) { intervals.forEach { minutes -> val isSelected = currentPollingInterval == minutes @@ -307,7 +301,7 @@ private fun NotificationTab( Box( modifier = Modifier .weight(1f) - .height(38.dp) + .height(34.dp) .clip(CornerRadius.shapeSmall) .background(bgColor) .elasticPressClickable { @@ -319,7 +313,7 @@ private fun NotificationTab( ) { Text( text = "${minutes}분", - style = MaterialTheme.typography.labelMedium.copy( + style = MaterialTheme.typography.labelSmall.copy( fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium ), color = textColor @@ -331,13 +325,13 @@ private fun NotificationTab( } } - // 키워드 알림 관리 섹션 + // 키워드 등록 입력창 item { SquircleCard( containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(14.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -346,7 +340,7 @@ private fun NotificationTab( imageVector = Icons.Outlined.NotificationsActive, contentDescription = null, tint = KeywordGold, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(17.dp) ) Text( text = "키워드 등록 & 관리", @@ -355,16 +349,15 @@ private fun NotificationTab( ) } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( - text = "등록한 키워드가 포함된 핫딜이 발견되면 즉시 푸시 알림을 발송합니다.", - style = MaterialTheme.typography.bodySmall, + text = "키워드가 포함된 핫딜 수집 시 즉시 알림을 발송합니다.", + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.5.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(10.dp)) - // 키워드 입력 필드 + 추가 버튼 일체형 Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -375,7 +368,7 @@ private fun NotificationTab( onValueChange = { keywordInput = it }, placeholder = { Text( - text = "키워드 입력 (예: 모니터, 칫솔, 그래픽카드)", + text = "키워드 입력 (예: 모니터, 그래픽카드)", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) @@ -399,36 +392,56 @@ private fun NotificationTab( } }, shape = CornerRadius.shapeNormal, - modifier = Modifier.height(52.dp), + modifier = Modifier.height(50.dp), enabled = keywordInput.isNotBlank() ) { Icon( imageVector = Icons.Filled.Add, contentDescription = "추가", - modifier = Modifier.size(18.dp) + modifier = Modifier.size(17.dp) ) - Spacer(modifier = Modifier.width(4.dp)) - Text(text = "추가", fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(3.dp)) + Text(text = "추가", fontWeight = FontWeight.Bold, fontSize = 13.sp) } } } } } - // 등록된 키워드 목록 + // 등록된 키워드 목록 (순서 조절 가능) if (activeKeywords.isNotEmpty()) { item { - Text( - text = "등록된 키워드 (${activeKeywords.size}개)", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "등록된 키워드 (${activeKeywords.size}개)", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "▲▼ 버튼으로 순서 조절", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } } - items(activeKeywords, key = { it.id }) { keyword -> + items( + count = activeKeywords.size, + key = { index -> activeKeywords[index].id } + ) { index -> + val keyword = activeKeywords[index] OneUIKeywordTile( keyword = keyword, + isFirst = index == 0, + isLast = index == activeKeywords.lastIndex, + onMoveUp = { viewModel.reorderKeywords(index, index - 1) }, + onMoveDown = { viewModel.reorderKeywords(index, index + 1) }, onToggle = { viewModel.toggleKeyword(keyword.id, !keyword.isEnabled) }, onDelete = { viewModel.deleteKeyword(keyword.id) } ) @@ -438,71 +451,76 @@ private fun NotificationTab( } // ============================================ -// 탭 2: 사이트 관리 (SitesTab) +// 탭 2: 사이트 관리 (SitesTab - 순서 조절 지원) // ============================================ @Composable private fun SitesTab(viewModel: MainViewModel) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsState() + val siteOrder by viewModel.siteOrder.collectAsState() LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) + .padding(horizontal = 14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(top = 6.dp, bottom = 24.dp) ) { item { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 4.dp, vertical = 4.dp), + .padding(horizontal = 4.dp, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = Icons.Outlined.Language, + imageVector = Icons.Outlined.SwapVert, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(8.dp)) - Column { + Spacer(modifier = Modifier.width(6.dp)) + Column(modifier = Modifier.weight(1f)) { Text( - text = "수집 대상 커뮤니티", + text = "수집 대상 커뮤니티 및 우선순위", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold ) Text( - text = "원하는 사이트 및 세부 게시판을 켜고 끌 수 있습니다.", - style = MaterialTheme.typography.bodySmall, + text = "▲▼ 버튼으로 순서를 변경하면 메인 필터 칩에 즉시 반영됩니다.", + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } - when (val state = uiState) { - is MainUiState.Success -> { - SiteType.entries.forEach { site -> - val configs = state.siteConfigs.filter { it.siteName == site.name } - item(key = site.name) { - OneUISiteBentoCard( - siteType = site, - configs = configs, - onToggle = { key, enabled -> viewModel.toggleSiteConfig(key, enabled) } - ) - } - } + val successState = uiState as? MainUiState.Success + if (successState != null) { + items( + count = siteOrder.size, + key = { index -> siteOrder[index].name } + ) { index -> + val site = siteOrder[index] + val configs = successState.siteConfigs.filter { it.siteName == site.name } + OneUISiteBentoCard( + siteType = site, + configs = configs, + isFirst = index == 0, + isLast = index == siteOrder.lastIndex, + onMoveUp = { viewModel.reorderSites(index, index - 1) }, + onMoveDown = { viewModel.reorderSites(index, index + 1) }, + onToggle = { key, enabled -> viewModel.toggleSiteConfig(key, enabled) } + ) } - else -> { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + } else { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } } } @@ -518,7 +536,6 @@ private fun MoreTab(viewModel: MainViewModel) { val scope = rememberCoroutineScope() var showDeleteDialog by remember { mutableStateOf(false) } var isCheckingUpdate by remember { mutableStateOf(false) } - var isDownloading by remember { mutableStateOf(false) } val toastEvent by viewModel.toastEvent.collectAsStateWithLifecycle(initialValue = null) @@ -531,11 +548,11 @@ private fun MoreTab(viewModel: MainViewModel) { LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - contentPadding = PaddingValues(top = 8.dp, bottom = 32.dp) + .padding(horizontal = 14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(top = 6.dp, bottom = 24.dp) ) { - // One UI 9 App Hero Info Card + // App Hero Info Card item { SquircleCard( containerColor = MaterialTheme.colorScheme.surface, @@ -544,12 +561,12 @@ private fun MoreTab(viewModel: MainViewModel) { Column( modifier = Modifier .fillMaxWidth() - .padding(20.dp), + .padding(18.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier - .size(64.dp) + .size(56.dp) .clip(CornerRadius.shapeSquircle) .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center @@ -558,27 +575,27 @@ private fun MoreTab(viewModel: MainViewModel) { imageVector = Icons.Filled.LocalFireDepartment, contentDescription = null, tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(36.dp) + modifier = Modifier.size(32.dp) ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) Text( text = "핫딜 알람 (HotDeal Alarm)", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(2.dp)) Text( text = "v${VersionManager.getCurrentVersion(context)} • One UI 9 Edition", - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(14.dp)) Button( onClick = { @@ -596,18 +613,19 @@ private fun MoreTab(viewModel: MainViewModel) { } }, shape = CornerRadius.shapeNormal, - modifier = Modifier.height(44.dp), - enabled = !isCheckingUpdate && !isDownloading + modifier = Modifier.height(42.dp), + enabled = !isCheckingUpdate ) { Icon( imageVector = Icons.Outlined.SystemUpdate, contentDescription = null, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(16.dp) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(6.dp)) Text( text = if (isCheckingUpdate) "확인 중..." else "업데이트 확인", - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, + fontSize = 13.sp ) } } @@ -620,7 +638,7 @@ private fun MoreTab(viewModel: MainViewModel) { containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(14.dp)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -629,7 +647,7 @@ private fun MoreTab(viewModel: MainViewModel) { imageVector = Icons.Outlined.Storage, contentDescription = null, tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(17.dp) ) Text( text = "데이터 및 캐시 관리", @@ -638,14 +656,14 @@ private fun MoreTab(viewModel: MainViewModel) { ) } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = "수집되어 로컬 DB에 캐싱된 모든 핫딜 데이터를 일괄 삭제합니다.", - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.5.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(12.dp)) FilledTonalButton( onClick = { showDeleteDialog = true }, @@ -654,15 +672,15 @@ private fun MoreTab(viewModel: MainViewModel) { containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer ), - modifier = Modifier.fillMaxWidth().height(48.dp) + modifier = Modifier.fillMaxWidth().height(44.dp) ) { Icon( imageVector = Icons.Outlined.DeleteSweep, contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = "수집 데이터 전체 비우기", fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(6.dp)) + Text(text = "수집 데이터 전체 비우기", fontWeight = FontWeight.Bold, fontSize = 13.sp) } } } @@ -695,7 +713,7 @@ private fun MoreTab(viewModel: MainViewModel) { } // ============================================ -// 서브 컴포넌트들 (One UI 9 UI Tiles) +// 서브 컴포넌트들 (순서 조절 버튼 포함) // ============================================ @Composable @@ -723,18 +741,18 @@ private fun BentoMiniTile( .clip(CornerRadius.shapeNormal) .background(bgColor) .then(if (onClick != null) Modifier.elasticPressClickable(onClick = onClick) else Modifier) - .padding(horizontal = 12.dp, vertical = 10.dp) + .padding(horizontal = 10.dp, vertical = 8.dp) ) { Column { Text( text = title, - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.5.sp), 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), + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold, fontSize = 12.5.sp), color = textColor ) } @@ -744,51 +762,88 @@ private fun BentoMiniTile( @Composable private fun OneUIKeywordTile( keyword: Keyword, + isFirst: Boolean, + isLast: Boolean, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, onToggle: () -> Unit, onDelete: () -> Unit ) { SquircleCard( + shape = CornerRadius.shapeNormal, containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 10.dp), + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { + // 순서 조절 상/하 버튼 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + IconButton( + onClick = onMoveUp, + enabled = !isFirst, + modifier = Modifier.size(22.dp) + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = "위로 이동", + tint = if (!isFirst) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + modifier = Modifier.size(16.dp) + ) + } + IconButton( + onClick = onMoveDown, + enabled = !isLast, + modifier = Modifier.size(22.dp) + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = "아래로 이동", + tint = if (!isLast) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.width(6.dp)) + Box( modifier = Modifier - .size(8.dp) + .size(7.dp) .background(KeywordGold, CircleShape) ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(8.dp)) Text( text = keyword.keyword, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold), modifier = Modifier.weight(1f) ) Switch( checked = keyword.isEnabled, onCheckedChange = { onToggle() }, - modifier = Modifier.scale(0.85f) + modifier = Modifier.scale(0.8f) ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(2.dp)) IconButton( onClick = onDelete, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(28.dp) ) { Icon( imageVector = Icons.Outlined.Delete, contentDescription = "삭제", tint = MaterialTheme.colorScheme.outline, - modifier = Modifier.size(18.dp) + modifier = Modifier.size(16.dp) ) } } @@ -799,6 +854,10 @@ private fun OneUIKeywordTile( private fun OneUISiteBentoCard( siteType: SiteType, configs: List, + isFirst: Boolean, + isLast: Boolean, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, onToggle: (String, Boolean) -> Unit ) { val siteColor = getSiteColor(siteType) @@ -806,43 +865,77 @@ private fun OneUISiteBentoCard( var isExpanded by remember { mutableStateOf(false) } SquircleCard( + shape = CornerRadius.shapeNormal, 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)) { + Column(modifier = Modifier.padding(12.dp)) { // 상단 마스터 행 Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { + // 순서 조절 버튼 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + IconButton( + onClick = onMoveUp, + enabled = !isFirst, + modifier = Modifier.size(22.dp) + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = "위로 이동", + tint = if (!isFirst) siteColor else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + modifier = Modifier.size(16.dp) + ) + } + IconButton( + onClick = onMoveDown, + enabled = !isLast, + modifier = Modifier.size(22.dp) + ) { + Icon( + imageVector = Icons.Filled.KeyboardArrowDown, + contentDescription = "아래로 이동", + tint = if (!isLast) siteColor else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.width(6.dp)) + // 브랜드 뱃지 Surface( shape = CornerRadius.shapeSmall, color = siteColor.copy(alpha = 0.14f), - modifier = Modifier.size(36.dp) + modifier = Modifier.size(32.dp) ) { Box(contentAlignment = Alignment.Center) { Text( text = siteType.displayName.take(1), - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.ExtraBold), + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.ExtraBold), color = siteColor ) } } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(10.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = siteType.displayName, - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), color = MaterialTheme.colorScheme.onSurface ) Text( text = "${configs.count { it.isEnabled }}/${configs.size}개 게시판 활성화", - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 11.sp), color = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -855,12 +948,12 @@ private fun OneUISiteBentoCard( onToggle(config.siteBoardKey, enableAll) } }, - modifier = Modifier.scale(0.9f) + modifier = Modifier.scale(0.85f) ) IconButton( onClick = { isExpanded = !isExpanded }, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(28.dp) ) { Icon( imageVector = if (isExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, @@ -879,13 +972,13 @@ private fun OneUISiteBentoCard( Column( modifier = Modifier .fillMaxWidth() - .padding(top = 12.dp) + .padding(top = 8.dp) ) { - Divider( + HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), thickness = 0.8.dp ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(6.dp)) siteType.boards.forEach { board -> val config = configs.find { it.boardName == board.id } @@ -895,19 +988,19 @@ private fun OneUISiteBentoCard( Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 4.dp), + .padding(vertical = 3.dp, horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = board.displayName, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 13.sp), color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f) ) Switch( checked = isEnabled, onCheckedChange = { onToggle(key, it) }, - modifier = Modifier.scale(0.8f) + modifier = Modifier.scale(0.75f) ) } }