diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt index 1ea7870..ee9337c 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/deallist/DealListScreen.kt @@ -3,7 +3,6 @@ package com.hotdeal.alarm.presentation.deallist import android.content.Intent import android.net.Uri import androidx.compose.animation.* -import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* @@ -12,6 +11,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* @@ -22,10 +23,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex @@ -38,8 +43,8 @@ import com.hotdeal.alarm.ui.theme.* import kotlinx.coroutines.launch /** - * One UI 9 & Material 3 Expressive Deal Feed Screen - * 실시간 Now 브리핑, 일체형 검색 & Sticky Pill 필터링을 지원하는 메인 핫딜 피드 + * One UI 9 & Material 3 Expressive Compact Deal Feed Screen + * 온디맨드 검색(In-AppBar Search)과 스크롤 반응형 상단바로 화면 시야 및 컨텐츠 노출 수를 극대화한 화면 */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,11 +54,13 @@ fun DealListScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val focusManager = LocalFocusManager.current - // Pull to Refresh (초고감도 설정) - val pullToRefreshState = rememberPullToRefreshState( - positionalThreshold = 40.dp - ) + // 스크롤 시 상단바 자동 축소/숨김 동작 + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + + // Pull to Refresh + val pullToRefreshState = rememberPullToRefreshState(positionalThreshold = 40.dp) var isRefreshing by remember { mutableStateOf(false) } val listState = rememberLazyListState() @@ -69,67 +76,129 @@ fun DealListScreen( } } - // 필터 상태 + // 검색 모드 및 필터 상태 + var isSearchActive by remember { mutableStateOf(false) } var searchText by remember { mutableStateOf("") } + val searchFocusRequester = remember { FocusRequester() } + var selectedSiteFilter by remember { mutableStateOf(null) } var showFavoritesOnly by remember { mutableStateOf(false) } var showPopularOnly by remember { mutableStateOf(false) } var showKeywordMatchOnly by remember { mutableStateOf(false) } + LaunchedEffect(isSearchActive) { + if (isSearchActive) { + searchFocusRequester.requestFocus() + } + } + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TopAppBar( title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - // One UI 9 App Logo Icon - Box( + if (isSearchActive) { + // 검색 모드일 때의 인라인 검색 텍스트 필드 + TextField( + value = searchText, + onValueChange = { searchText = it }, + placeholder = { + Text( + text = "키워드, 브랜드, 상품명 검색...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + textStyle = MaterialTheme.typography.bodyLarge, modifier = Modifier - .size(36.dp) - .clip(CornerRadius.shapeSmall) - .background(MaterialTheme.colorScheme.primary), - contentAlignment = Alignment.Center + .fillMaxWidth() + .focusRequester(searchFocusRequester) + ) + } else { + // 기본 상단 타이틀 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Icon( - imageVector = Icons.Filled.LocalFireDepartment, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(20.dp) - ) - } + Box( + modifier = Modifier + .size(32.dp) + .clip(CornerRadius.shapeMicro) + .background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.LocalFireDepartment, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(18.dp) + ) + } - Column { Text( text = "핫딜 알람", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - letterSpacing = (-0.3).sp + letterSpacing = (-0.2).sp + ) + } + } + }, + navigationIcon = { + if (isSearchActive) { + IconButton(onClick = { + isSearchActive = false + searchText = "" + }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "검색 닫기", + tint = MaterialTheme.colorScheme.onSurface ) } } }, actions = { - // 실시간 상태 요약 캡슐 - if (uiState is MainUiState.Success) { - val activeSitesCount = (uiState as MainUiState.Success).siteConfigs.count { it.isEnabled } - NowStatusPill( - text = if (activeSitesCount > 0) "${activeSitesCount}개 사이트 감시 중" else "모니터링 대기", - isLive = activeSitesCount > 0, - accentColor = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(4.dp)) - } + if (isSearchActive) { + if (searchText.isNotEmpty()) { + IconButton(onClick = { searchText = "" }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "검색어 지우기", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else { + // 1. 검색 버튼 (누르면 검색 모드로 전환) + IconButton(onClick = { isSearchActive = true }) { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = "검색", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } - IconButton(onClick = { viewModel.refresh() }) { - Icon( - imageVector = Icons.Outlined.Refresh, - contentDescription = "새로고침", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + // 2. 새로고침 버튼 + IconButton(onClick = { viewModel.refresh() }) { + Icon( + imageVector = Icons.Outlined.Refresh, + contentDescription = "새로고침", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } }, + scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = MaterialTheme.colorScheme.surface @@ -150,11 +219,12 @@ fun DealListScreen( containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, shape = CircleShape, - modifier = Modifier.navigationBarsPadding() + modifier = Modifier.size(44.dp).navigationBarsPadding() ) { Icon( imageVector = Icons.Filled.KeyboardArrowUp, - contentDescription = "맨 위로" + contentDescription = "맨 위로", + modifier = Modifier.size(22.dp) ) } } @@ -173,64 +243,18 @@ fun DealListScreen( .consumeWindowInsets(paddingValues) ) { // ============================================ - // 1. One UI 9 Seamless Search Field - // ============================================ - OutlinedTextField( - value = searchText, - onValueChange = { searchText = it }, - placeholder = { - Text( - text = "키워드, 브랜드, 상품명 검색...", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.55f) - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp), - singleLine = true, - shape = CornerRadius.shapeNormal, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Search, - contentDescription = "검색", - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - modifier = Modifier.size(20.dp) - ) - }, - trailingIcon = { - if (searchText.isNotEmpty()) { - IconButton(onClick = { searchText = "" }) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = "지우기", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) - ) - } - } - }, - 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) - ) - ) - - // ============================================ - // 2. One UI 9 Sticky Horizontal Pill Filter Bar + // 슬림 가로 스크롤 Sticky Pill Filter Bar // ============================================ Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 6.dp) + .padding(vertical = 4.dp) .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = Modifier.width(8.dp)) - // "전체" 필터 칩 FilterPillChip( selected = selectedSiteFilter == null && !showFavoritesOnly && !showPopularOnly && !showKeywordMatchOnly, onClick = { @@ -243,7 +267,6 @@ fun DealListScreen( accentColor = MaterialTheme.colorScheme.primary ) - // "🔥 HOT 인기" 필터 칩 FilterPillChip( selected = showPopularOnly, onClick = { @@ -257,7 +280,6 @@ fun DealListScreen( accentColor = HotDealFlameColor ) - // "⭐ 내 키워드" 필터 칩 FilterPillChip( selected = showKeywordMatchOnly, onClick = { @@ -271,7 +293,6 @@ fun DealListScreen( accentColor = KeywordGold ) - // "❤️ 즐겨찾기" 필터 칩 FilterPillChip( selected = showFavoritesOnly, onClick = { @@ -285,7 +306,6 @@ fun DealListScreen( accentColor = FavoriteColor ) - // 사이트별 개별 필터 칩들 (뽐뿌, 클리앙, 루리웹, 쿨엔조이, 아카라이브) SiteType.entries.forEach { siteType -> val siteColor = getSiteColor(siteType) FilterPillChip( @@ -302,7 +322,7 @@ fun DealListScreen( } // ============================================ - // 3. 핫딜 목록 본문 (List Content) + // 핫딜 목록 본문 (List Content) // ============================================ when (val state = uiState) { is MainUiState.Loading -> { @@ -347,35 +367,11 @@ fun DealListScreen( ) } } else { - // 리스트 상단 카운터 바 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 18.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "총 ${filteredDeals.size}개의 핫딜", - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) - ) - - if (selectedSiteFilter != null) { - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = "• ${selectedSiteFilter?.displayName}", - style = MaterialTheme.typography.labelSmall, - color = getSiteColor(selectedSiteFilter) - ) - } - } - LazyColumn( modifier = Modifier.fillMaxSize(), state = listState, - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(7.dp) ) { items( items = filteredDeals, @@ -394,7 +390,7 @@ fun DealListScreen( } item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -409,7 +405,7 @@ fun DealListScreen( } } - // One UI 9 Pull to Refresh 인디케이터 + // Pull to Refresh 인디케이터 val progress = pullToRefreshState.progress val showIndicator = pullToRefreshState.isRefreshing || progress > 0 @@ -418,7 +414,7 @@ fun DealListScreen( state = pullToRefreshState, modifier = Modifier .align(Alignment.TopCenter) - .padding(top = 90.dp) + .padding(top = 50.dp) .zIndex(999f), containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.primary @@ -436,7 +432,7 @@ fun DealListScreen( } /** - * One UI 9 Capsule Pill Filter Chip + * One UI 9 Compact Capsule Pill Filter Chip */ @Composable private fun FilterPillChip( @@ -446,7 +442,7 @@ private fun FilterPillChip( accentColor: Color ) { val bgColor = if (selected) { - accentColor.copy(alpha = 0.16f) + accentColor.copy(alpha = 0.15f) } else { MaterialTheme.colorScheme.surface } @@ -454,13 +450,13 @@ private fun FilterPillChip( val textColor = if (selected) { accentColor } else { - MaterialTheme.colorScheme.onSurfaceVariant + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) } val borderColor = if (selected) { - accentColor.copy(alpha = 0.6f) + accentColor.copy(alpha = 0.55f) } else { - MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f) + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) } Surface( @@ -468,17 +464,18 @@ private fun FilterPillChip( color = bgColor, border = androidx.compose.foundation.BorderStroke(0.8.dp, borderColor), modifier = Modifier - .height(34.dp) + .height(29.dp) .elasticPressClickable(onClick = onClick) ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp) + modifier = Modifier.padding(horizontal = 11.dp, vertical = 4.dp) ) { Text( text = label, - style = MaterialTheme.typography.labelMedium.copy( - fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, + fontSize = 11.5.sp ), color = textColor )