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 a5da4f3..d7d6bdc 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,6 +3,7 @@ package com.hotdeal.alarm.presentation.deallist import android.content.Intent import android.net.Uri import androidx.compose.animation.* +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* @@ -44,9 +45,9 @@ import kotlinx.coroutines.launch /** * One UI 9 & Material 3 Expressive Compact Deal Feed Screen - * 온디맨드 검색(In-AppBar Search)과 스크롤 반응형 상단바로 화면 시야 및 컨텐츠 노출 수를 극대화한 화면 + * 온디맨드 검색(In-AppBar Search)과 실시간 신규 핫딜 자동 상단 동기화 화면 */ -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun DealListScreen( viewModel: MainViewModel, @@ -70,6 +71,17 @@ fun DealListScreen( derivedStateOf { listState.firstVisibleItemIndex > 3 } } + // 신규 수집 핫딜 자동 상단 노출 상태 + var previousTopDealId by remember { mutableStateOf(null) } + var hasNewUnseenDeals by remember { mutableStateOf(false) } + + // 사용자가 최상단으로 올라오면 새 핫딜 배너 숨김 + LaunchedEffect(listState.firstVisibleItemIndex) { + if (listState.firstVisibleItemIndex == 0) { + hasNewUnseenDeals = false + } + } + LaunchedEffect(isRefreshingByVm) { if (!isRefreshingByVm) { pullToRefreshState.endRefresh() @@ -98,7 +110,6 @@ fun DealListScreen( TopAppBar( title = { if (isSearchActive) { - // 검색 모드일 때의 인라인 검색 텍스트 필드 TextField( value = searchText, onValueChange = { searchText = it }, @@ -124,7 +135,6 @@ fun DealListScreen( .focusRequester(searchFocusRequester) ) } else { - // 기본 상단 타이틀 Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -179,7 +189,6 @@ fun DealListScreen( } } } else { - // 1. 검색 버튼 (누르면 검색 모드로 전환) IconButton(onClick = { isSearchActive = true }) { Icon( imageVector = Icons.Outlined.Search, @@ -188,7 +197,6 @@ fun DealListScreen( ) } - // 2. 새로고침 버튼 IconButton(onClick = { viewModel.refresh() }) { Icon( imageVector = Icons.Outlined.Refresh, @@ -348,6 +356,25 @@ fun DealListScreen( } } + // 신규 파싱 핫딜 실시간 최상단 자동 노출 감지 + LaunchedEffect(filteredDeals.firstOrNull()?.id) { + val currentTop = filteredDeals.firstOrNull() + if (currentTop != null) { + if (previousTopDealId != null && previousTopDealId != currentTop.id) { + // 신규 핫딜 감지 + if (listState.firstVisibleItemIndex <= 1) { + // 사용자가 상단에 머물고 있으면 즉시 최상단(0)으로 부드럽게 스크롤하여 새 핫딜 바로 노출! + listState.animateScrollToItem(0) + hasNewUnseenDeals = false + } else { + // 사용자가 아래 글을 읽고 있는 경우 배너 표시 + hasNewUnseenDeals = true + } + } + previousTopDealId = currentTop.id + } + } + if (filteredDeals.isEmpty()) { if (state.deals.isEmpty()) { NoDealsState(onRefresh = { viewModel.refresh() }) @@ -367,30 +394,77 @@ fun DealListScreen( ) } } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = listState, - contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(7.dp) - ) { - items( - items = filteredDeals, - key = { it.id } - ) { deal -> - DealItem( - deal = deal, - onClick = { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url)) - context.startActivity(intent) - }, - onFavoriteToggle = { dealId -> - viewModel.toggleFavorite(dealId) - } - ) + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(7.dp) + ) { + items( + items = filteredDeals, + key = { it.id } + ) { deal -> + DealItem( + deal = deal, + modifier = Modifier.animateItemPlacement(), + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deal.url)) + context.startActivity(intent) + }, + onFavoriteToggle = { dealId -> + viewModel.toggleFavorite(dealId) + } + ) + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + } } - item { - Spacer(modifier = Modifier.height(16.dp)) + // 새 핫딜 도착 시 상단 플로팅 칩 알림 (아래 스크롤 중일 때) + androidx.compose.animation.AnimatedVisibility( + visible = hasNewUnseenDeals, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 8.dp) + .zIndex(10f) + ) { + Surface( + shape = CornerRadius.shapePill, + color = MaterialTheme.colorScheme.primary, + shadowElevation = 6.dp, + modifier = Modifier.elasticPressClickable { + scope.launch { + listState.animateScrollToItem(0) + hasNewUnseenDeals = false + } + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp) + ) { + Icon( + imageVector = Icons.Filled.ArrowUpward, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(13.dp) + ) + Text( + text = "새로운 핫딜 도착 (터치하여 보기)", + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.Bold, + fontSize = 11.5.sp + ), + color = MaterialTheme.colorScheme.onPrimary + ) + } + } } } }