diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt index 5c016aa9..d34b247e 100644 --- a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt @@ -31,7 +31,7 @@ data class RoomRecruitingResponse( @Serializable data class RecommendRoomResponse( @SerialName("roomId") val roomId: Int, - @SerialName("roomImageUrl") val roomImageUrl: String?, + @SerialName("bookImageUrl") val bookImageUrl: String?, @SerialName("roomName") val roomName: String, @SerialName("memberCount") val memberCount: Int, @SerialName("recruitCount") val recruitCount: Int, diff --git a/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt index ec81fc0e..3eedc1e2 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/bottomsheet/CustomBottomSheet.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.utils.rooms.advancedImePadding import kotlinx.coroutines.launch private const val BOTTOM_SHEET_HIDDEN_OFFSET = 300f @@ -93,6 +94,7 @@ fun CustomBottomSheet( modifier = Modifier .fillMaxWidth() .offset(y = (offsetY + animatableOffset.value).dp) + .advancedImePadding() .background( color = colors.DarkGrey, shape = RoundedCornerShape(topEnd = 12.dp, topStart = 12.dp) diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt index f2bc6b1b..1a109d7e 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/ActionBookButton.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -51,16 +52,25 @@ fun ActionBookButton( maxLines = 1 ) + // 저자명과 "저"를 분리 Text( - text = bookAuthor + stringResource(R.string.author), + text = bookAuthor, style = typography.info_r400_s12_h24, color = colors.Grey, - modifier = Modifier.width(100.dp), + modifier = Modifier + .widthIn(max = 80.dp) + .padding(start = 8.dp), textAlign = TextAlign.Right, overflow = TextOverflow.Ellipsis, maxLines = 1 ) + Text( + text = stringResource(R.string.author), + style = typography.info_r400_s12_h24, + color = colors.Grey, + ) + Icon( painter = painterResource(R.drawable.ic_chevron), contentDescription = null, diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt index 364cac9f..9619852b 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt @@ -18,11 +18,12 @@ fun GenreChipRow( modifier: Modifier = Modifier.width(4.dp), genres: List, selectedIndex: Int, - onSelect: (Int) -> Unit + onSelect: (Int) -> Unit, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Center ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center + horizontalArrangement = horizontalArrangement ) { genres.forEachIndexed { idx, genre -> OptionChipButton( diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt index 383e5f61..c29255df 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt @@ -16,11 +16,12 @@ import com.texthip.thip.ui.theme.ThipTheme fun SubGenreChipGrid( subGenres: List, selectedGenres: List, - onGenreToggle: (String) -> Unit + onGenreToggle: (String) -> Unit, + horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally ) { FlowRow( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(8.dp, horizontalAlignment), verticalArrangement = Arrangement.spacedBy(8.dp) ) { subGenres.forEach { genre -> diff --git a/app/src/main/java/com/texthip/thip/ui/common/forms/SingleDigitTextBox.kt b/app/src/main/java/com/texthip/thip/ui/common/forms/SingleDigitTextBox.kt index 380fb8a6..2b618894 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/forms/SingleDigitTextBox.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/forms/SingleDigitTextBox.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.common.forms -import android.view.KeyEvent import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -12,16 +11,16 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.key.type +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -39,6 +38,15 @@ fun SingleDigitBox( containerColor: Color = colors.darkGray01, borderColor: Color = Color.Transparent ) { + // TextFieldValue로 커서 위치 제어 + val textFieldValue = remember(value) { + val displayText = value.ifEmpty { "\u200B" } // Zero Width Space + TextFieldValue( + text = displayText, + selection = TextRange(displayText.length) // 커서를 맨 끝에 위치 + ) + } + val myStyle = typography.smalltitle_sb600_s18_h24.copy( lineHeight = 20.sp, textAlign = TextAlign.Center, @@ -53,37 +61,32 @@ fun SingleDigitBox( contentAlignment = Alignment.Center ) { BasicTextField( - value = value, - onValueChange = { input -> - val filtered = input.filter { it.isDigit() }.take(1) + value = textFieldValue, + onValueChange = { newValue -> + val cleaned = newValue.text.replace("\u200B", "") + val filtered = cleaned.filter { it.isDigit() }.take(1) + + // 백스페이스 감지: Zero Width Space가 지워졌을 때 + if (newValue.text.isEmpty() && value.isEmpty()) { + onBackspace?.invoke() + return@BasicTextField + } + onValueChange(filtered) }, - textStyle = myStyle, + textStyle = myStyle.copy( + color = if (value.isEmpty()) Color.Transparent else colors.White + ), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), - modifier = modifier - .background(containerColor, RoundedCornerShape(12.dp)) - .border(1.dp, borderColor, RoundedCornerShape(12.dp)) - .onKeyEvent { keyEvent -> - if (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL && - keyEvent.type == KeyEventType.KeyDown - ) { - if (value.isEmpty()) { - onBackspace?.invoke() - true - } else { - false - } - } else { - false - } - }, cursorBrush = SolidColor(colors.NeonGreen), decorationBox = { innerTextField -> Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { innerTextField() } + ) { + innerTextField() + } } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt b/app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt index 1fbec027..12e038ad 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/topappbar/GradationTopAppBar.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.common.topappbar +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,6 +10,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -20,14 +26,39 @@ import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.ui.common.view.CountingBar import com.texthip.thip.ui.theme.ThipTheme.colors +import kotlinx.coroutines.delay @Composable fun GradationTopAppBar( isImageVisible: Boolean = false, count: Int = 0, + autoHideCount: Boolean? = null, + countDisplayDurationMs: Long = 5000L, onLeftClick: () -> Unit, - onRightClick: () -> Unit, + onRightClick: () -> Unit = {}, ) { + var isCountVisible by remember { mutableStateOf(isImageVisible) } + + // autoHideCount가 null이 아닐 때만 자동 숨김 로직 실행 + LaunchedEffect(autoHideCount, count) { + when (autoHideCount) { + true -> { + if (count > 0) { + isCountVisible = true + delay(countDisplayDurationMs) + isCountVisible = false + } + } + false -> { + isCountVisible = true + } + null -> { + // 기존 동작 유지: isImageVisible 파라미터 사용 + isCountVisible = isImageVisible + } + } + } + val bgColor = Brush.verticalGradient( colors = listOf( colors.Black, @@ -54,15 +85,17 @@ fun GradationTopAppBar( ) } - if (isImageVisible) { - CountingBar( - modifier = Modifier - .align(Alignment.Center), - content = stringResource(R.string.reading_user_num, count) - ) + Column { + AnimatedVisibility( + visible = isCountVisible && count > 0 + ) { + CountingBar( + content = stringResource(R.string.reading_user_num, count) + ) + } } - IconButton( + /*IconButton( onClick = onRightClick, modifier = Modifier.align(Alignment.CenterEnd) ) { @@ -71,7 +104,7 @@ fun GradationTopAppBar( contentDescription = "More Options", tint = Color.Unspecified ) - } + }*/ } } diff --git a/app/src/main/java/com/texthip/thip/ui/common/topappbar/LogoTopAppBar.kt b/app/src/main/java/com/texthip/thip/ui/common/topappbar/LogoTopAppBar.kt index 411fb48b..14fcd678 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/topappbar/LogoTopAppBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/topappbar/LogoTopAppBar.kt @@ -32,7 +32,7 @@ fun LogoTopAppBar( val rightIcon = if (hasNotification) { painterResource(R.drawable.ic_notice_yes) } else { - painterResource(R.drawable.ic_notice) + painterResource(R.drawable.ic_notice_no) } Box( diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt index 41177047..5618a7f5 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedCommentScreen.kt @@ -1,5 +1,9 @@ package com.texthip.thip.ui.feed.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures @@ -79,6 +83,7 @@ fun FeedCommentScreen( onNavigateBack: () -> Unit = {}, onNavigateToFeedEdit: (Int) -> Unit = {}, onNavigateToUserProfile: (userId: Long) -> Unit = {}, + onNavigateToBookDetail: (String) -> Unit = {}, feedDetailViewModel: FeedDetailViewModel = hiltViewModel(), commentsViewModel: CommentsViewModel = hiltViewModel() ) { @@ -223,7 +228,9 @@ fun FeedCommentScreen( ActionBookButton( bookTitle = feedDetail.bookTitle, bookAuthor = feedDetail.bookAuthor, - onClick = {} + onClick = { + onNavigateToBookDetail(feedDetail.isbn) + } ) } Text( @@ -398,17 +405,27 @@ fun FeedCommentScreen( } ) } - } - // 신고 완료 토스트 - if (showToast) { - ToastWithDate( - message = "게시글 신고를 완료했어요.", + // 신고 완료 토스트 + AnimatedVisibility( + visible = showToast, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), modifier = Modifier .align(Alignment.TopCenter) .padding(horizontal = 20.dp, vertical = 16.dp) .zIndex(2f) - ) + ) { + ToastWithDate( + message = "게시글 신고를 완료했어요." + ) + } } if (isBottomSheetVisible) { diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index 6f18edc1..2bbc10f6 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -72,8 +72,11 @@ fun FeedScreen( onNavigateToBookDetail: (String) -> Unit = {}, resultFeedId: Long? = null, onNavigateToUserProfile: (userId: Long) -> Unit = {}, + onNavigateToNotification: () -> Unit = {}, + refreshFeed: Boolean? = null, onNavigateToOthersSubscription: (userId: Long) -> Unit = {}, onResultConsumed: () -> Unit = {}, + onRefreshConsumed: () -> Unit = {}, navController: NavHostController, feedViewModel: FeedViewModel = hiltViewModel(), ) { @@ -84,13 +87,19 @@ fun FeedScreen( val feedTabTitles = listOf(stringResource(R.string.feed), stringResource(R.string.my_feed)) - // 무한 스크롤 로직 - val listState = rememberLazyListState() + // 탭별로 별도의 스크롤 상태 관리 + val allFeedListState = rememberLazyListState() + val myFeedListState = rememberLazyListState() + val currentListState = when (feedUiState.selectedTabIndex) { + 0 -> allFeedListState + 1 -> myFeedListState + else -> allFeedListState + } // 무한 스크롤 로직 - val shouldLoadMore by remember(feedUiState.canLoadMoreCurrentTab, feedUiState.isLoadingMore) { + val shouldLoadMore by remember(feedUiState.canLoadMoreCurrentTab, feedUiState.isLoadingMore, feedUiState.selectedTabIndex) { derivedStateOf { - val layoutInfo = listState.layoutInfo + val layoutInfo = currentListState.layoutInfo val totalItems = layoutInfo.totalItemsCount val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 @@ -119,7 +128,12 @@ fun FeedScreen( } LaunchedEffect(Unit) { - feedViewModel.refreshData() + feedViewModel.resetToInitialState() + } + + // 탭 변경 시 해당 탭의 스크롤을 최상단으로 부드럽게 이동 + LaunchedEffect(feedUiState.selectedTabIndex) { + currentListState.scrollToItem(0) } LaunchedEffect(resultFeedId) { @@ -137,6 +151,16 @@ fun FeedScreen( if (showProgressBar) { showProgressBar = false } + feedViewModel.refreshData() + } + } + } + + LaunchedEffect(refreshFeed) { + if (refreshFeed == true) { + onRefreshConsumed() + if (resultFeedId == null) { + feedViewModel.refreshData() } } } @@ -157,8 +181,8 @@ fun FeedScreen( Box(modifier = Modifier.fillMaxSize()) { PullToRefreshBox( - isRefreshing = feedUiState.isRefreshing, - onRefresh = { feedViewModel.refreshCurrentTab() } + isRefreshing = feedUiState.isPullToRefreshing, + onRefresh = { feedViewModel.pullToRefresh() } ) { Column( modifier = Modifier.fillMaxSize() @@ -167,7 +191,7 @@ fun FeedScreen( leftIcon = painterResource(R.drawable.ic_plusfriend), hasNotification = false, onLeftClick = {}, - onRightClick = {}, + onRightClick = onNavigateToNotification, ) Spacer(modifier = Modifier.height(32.dp)) HeaderMenuBarTab( @@ -178,7 +202,7 @@ fun FeedScreen( // 스크롤 영역 전체 LazyColumn( - state = listState, + state = currentListState, modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp) ) { @@ -405,6 +429,19 @@ fun FeedScreen( icon = painterResource(id = R.drawable.ic_write), onClick = onNavigateToFeedWrite ) + + // 탭 전환 시 화면 가운데 로딩 인디케이터 + if (feedUiState.isRefreshing) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + color = colors.White, + modifier = Modifier.size(48.dp) + ) + } + } } } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt index c4923e47..efb2cd97 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt @@ -304,10 +304,11 @@ fun FeedWriteContent( ) Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( - modifier = Modifier.width(18.dp), + modifier = Modifier.width(8.dp), genres = uiState.categories.map { it.category }, selectedIndex = uiState.selectedCategoryIndex, - onSelect = onSelectCategory + onSelect = onSelectCategory, + horizontalArrangement = Arrangement.Start ) Spacer(modifier = Modifier.height(12.dp)) if (uiState.selectedCategoryIndex != -1) { @@ -316,7 +317,8 @@ fun FeedWriteContent( SubGenreChipGrid( subGenres = uiState.availableTags, selectedGenres = uiState.selectedTags, - onGenreToggle = onToggleTag + onGenreToggle = onToggleTag, + horizontalAlignment = Alignment.Start ) Row( modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt index c1cea5d8..a760a108 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt @@ -1,5 +1,9 @@ package com.texthip.thip.ui.feed.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -102,19 +106,25 @@ fun MySubscriptionContent( } Box(modifier = Modifier.fillMaxSize()) { - if (uiState.showToast) { - Box( - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .align(Alignment.TopCenter) - .padding(horizontal = 15.dp, vertical = 15.dp), - ) { - ToastWithDate( - message = uiState.toastMessage, - modifier = Modifier.fillMaxWidth() - ) - } + AnimatedVisibility( + visible = uiState.showToast, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 15.dp, vertical = 15.dp) + .zIndex(1f) + ) { + ToastWithDate( + message = uiState.toastMessage, + modifier = Modifier.fillMaxWidth() + ) } Column( diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt index c158b0d3..e3d0ae5a 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -24,7 +24,8 @@ data class FeedUiState( val recentWriters: List = emptyList(), val myFeedInfo: FeedMineInfoResponse? = null, val isLoading: Boolean = false, - val isRefreshing: Boolean = false, + val isRefreshing: Boolean = false, // 탭 전환용 로딩 + val isPullToRefreshing: Boolean = false, // Pull to refresh용 로딩 val isLoadingMore: Boolean = false, val isLastPageAllFeeds: Boolean = false, val isLastPageMyFeeds: Boolean = false, @@ -76,12 +77,16 @@ class FeedViewModel @Inject constructor( when (index) { 0 -> { - loadAllFeeds(isInitial = true) + // 항상 새로고침 (인디케이터 표시) + refreshCurrentTab() } 1 -> { - loadMyFeeds(isInitial = true) - fetchMyFeedInfo() + // 항상 새로고침 (인디케이터 표시) + refreshCurrentTab() + if (_uiState.value.myFeedInfo == null) { + fetchMyFeedInfo() + } } } } @@ -192,6 +197,18 @@ class FeedViewModel @Inject constructor( } } + fun pullToRefresh() { + viewModelScope.launch { + updateState { it.copy(isPullToRefreshing = true) } + + when (_uiState.value.selectedTabIndex) { + 0 -> refreshAllFeeds() + 1 -> refreshMyFeeds() + } + updateState { it.copy(isPullToRefreshing = false) } + } + } + private suspend fun refreshAllFeeds() { allFeedsNextCursor = null @@ -201,7 +218,6 @@ class FeedViewModel @Inject constructor( updateState { it.copy( allFeeds = response.feedList, - isRefreshing = false, isLastPageAllFeeds = response.isLast, error = null ) @@ -210,7 +226,6 @@ class FeedViewModel @Inject constructor( updateState { it.copy( allFeeds = emptyList(), - isRefreshing = false, isLastPageAllFeeds = true ) } @@ -218,7 +233,6 @@ class FeedViewModel @Inject constructor( }.onFailure { exception -> updateState { it.copy( - isRefreshing = false, error = exception.message ) } @@ -234,7 +248,6 @@ class FeedViewModel @Inject constructor( updateState { it.copy( myFeeds = response.feedList, - isRefreshing = false, isLastPageMyFeeds = response.isLast, error = null ) @@ -243,7 +256,6 @@ class FeedViewModel @Inject constructor( updateState { it.copy( myFeeds = emptyList(), - isRefreshing = false, isLastPageMyFeeds = true ) } @@ -251,7 +263,6 @@ class FeedViewModel @Inject constructor( }.onFailure { exception -> updateState { it.copy( - isRefreshing = false, error = exception.message ) } @@ -268,6 +279,24 @@ class FeedViewModel @Inject constructor( } fun refreshData() { + loadAllFeeds() + fetchRecentWriters() + } + + fun resetToInitialState() { + // 탭과 데이터를 모두 초기 상태로 리셋 + updateState { + it.copy( + selectedTabIndex = 0, + allFeeds = emptyList(), + myFeeds = emptyList(), + isLastPageAllFeeds = false, + isLastPageMyFeeds = false + ) + } + allFeedsNextCursor = null + myFeedsNextCursor = null + loadAllFeeds(isInitial = true) fetchRecentWriters() } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt index 97bb7041..3563678c 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt @@ -56,7 +56,7 @@ data class FeedWriteUiState( get() = if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.size) { categories[selectedCategoryIndex].tagList } else { - emptyList() + categories.flatMap { it.tagList }.distinct() } // 현재 선택된 카테고리 이름 diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt index 0b4cb315..f7dccf48 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -326,8 +326,7 @@ class FeedWriteViewModel @Inject constructor( fun selectCategory(index: Int) { updateState { it.copy( - selectedCategoryIndex = index, - selectedTags = emptyList() // 카테고리 변경 시 태그 초기화 + selectedCategoryIndex = index ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt index b933856d..1b478a5c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupDatePicker.kt @@ -49,9 +49,7 @@ fun GroupDatePicker( } Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + verticalAlignment = Alignment.CenterVertically ) { Row( verticalAlignment = Alignment.CenterVertically @@ -72,6 +70,7 @@ fun GroupDatePicker( ) Spacer(modifier = Modifier.width(2.dp)) Text( + modifier = Modifier.padding(end = 8.dp), text = stringResource(R.string.group_year), style = typography.info_r400_s12, color = colors.White @@ -97,6 +96,7 @@ fun GroupDatePicker( ) Spacer(modifier = Modifier.width(2.dp)) Text( + modifier = Modifier.padding(end = 8.dp), text = stringResource(R.string.group_month), style = typography.info_r400_s12, color = colors.White diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt index c36d5f4b..49d41c13 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupRoomDurationPicker.kt @@ -33,18 +33,19 @@ fun GroupRoomDurationPicker( onDateRangeSelected: (LocalDate, LocalDate) -> Unit = { _, _ -> } ) { val today = LocalDate.now() + val tomorrow = today.plusDays(1) val maxDate = today.plusMonths(12) var isInitialized by rememberSaveable { mutableStateOf(false) } - var startDate by rememberSaveable { mutableStateOf(today) } - var endDate by rememberSaveable { mutableStateOf(today.plusDays(1)) } + var startDate by rememberSaveable { mutableStateOf(tomorrow) } + var endDate by rememberSaveable { mutableStateOf(tomorrow.plusDays(1)) } var isPickerTouched by rememberSaveable { mutableStateOf(false) } - // 첫 시작 시에만 모든 날짜를 오늘 기준으로 초기화 + // 첫 시작 시에만 모든 날짜를 내일 기준으로 초기화 LaunchedEffect(Unit) { if (!isInitialized) { - startDate = today - endDate = today.plusDays(1) + startDate = tomorrow + endDate = tomorrow.plusDays(1) isInitialized = true } } @@ -63,7 +64,7 @@ fun GroupRoomDurationPicker( // 날짜 유효성 검사 및 자동 조정 LaunchedEffect(startDate) { val adjustedStartDate = when { - startDate.isBefore(today) -> today + startDate.isBefore(tomorrow) -> tomorrow startDate.isAfter(maxDate) -> maxDate else -> startDate } @@ -107,14 +108,13 @@ fun GroupRoomDurationPicker( // 시작 날짜 Picker GroupDatePicker( selectedDate = startDate, - minDate = today, + minDate = tomorrow, maxDate = maxDate, onDateSelected = { newDate -> startDate = newDate }, modifier = Modifier .weight(1f) - .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( onPress = { isPickerTouched = true } @@ -127,20 +127,19 @@ fun GroupRoomDurationPicker( text = "~", style = typography.info_r400_s12, color = colors.White, - modifier = Modifier.padding(horizontal = 10.dp) + modifier = Modifier.padding(horizontal = 4.dp) ) // 끝 날짜 Picker GroupDatePicker( selectedDate = endDate, - minDate = today, + minDate = tomorrow, maxDate = maxDate, onDateSelected = { newDate -> endDate = newDate }, modifier = Modifier .weight(1f) - .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( onPress = { isPickerTouched = true } @@ -196,7 +195,6 @@ fun GroupRoomDurationPicker( fun MeetingDurationPickerPreview() { ThipTheme { GroupRoomDurationPicker { startDate, endDate -> - println("Selected date range: $startDate to $endDate") } } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 54964d1c..f4fff79a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -146,10 +146,11 @@ fun GroupMakeRoomContent( ) Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( - modifier = Modifier.width(18.dp), + modifier = Modifier.width(12.dp), genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, - onSelect = onSelectGenre + onSelect = onSelectGenre, + horizontalArrangement = Arrangement.Start ) Spacer(modifier = Modifier.height(12.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt index f31bc1eb..f366f0ad 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomChatScreen.kt @@ -261,16 +261,32 @@ fun GroupRoomChatContent( } } - when (activeToast) { - ToastType.DAILY_GREETING_LIMIT -> { - ToastWithDate(color = colors.Red) - } + AnimatedVisibility( + visible = activeToast != null, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(3f) + ) { + when (activeToast) { + ToastType.DAILY_GREETING_LIMIT -> { + ToastWithDate(color = colors.Red) + } - ToastType.FIRST_WRITE -> { - ToastWithDate() - } + ToastType.FIRST_WRITE -> { + ToastWithDate() + } - null -> {} + null -> {} + } } } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index d3154c0c..c93bab84 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -1,5 +1,9 @@ package com.texthip.thip.ui.group.room.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -242,8 +246,7 @@ fun GroupRoomRecruitContent( Row( Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + verticalAlignment = Alignment.CenterVertically ) { //모집 기간 Column { @@ -278,7 +281,7 @@ fun GroupRoomRecruitContent( //참여 인원 Column( verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(end = 18.dp) + modifier = Modifier.padding(start = 90.dp) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -296,7 +299,8 @@ fun GroupRoomRecruitContent( ) } Row( - modifier = Modifier.padding(top = 12.dp), + modifier = Modifier + .padding(top = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -339,10 +343,7 @@ fun GroupRoomRecruitContent( ) Spacer(Modifier.width(4.dp)) Text( - text = detail.recruitEndDate.replace( - "뒤", - "남음" - ), + text = detail.recruitEndDate, style = typography.info_m500_s12, color = colors.NeonGreen ) @@ -402,7 +403,7 @@ fun GroupRoomRecruitContent( participants = rec.memberCount, maxParticipants = rec.recruitCount, endDate = rec.recruitEndDate, - imageUrl = rec.roomImageUrl, + imageUrl = rec.bookImageUrl, onClick = { onRecommendationClick(rec) } ) } @@ -461,13 +462,23 @@ fun GroupRoomRecruitContent( } // 토스트 팝업 - if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { + AnimatedVisibility( + visible = uiState.showToast && !uiState.shouldNavigateToGroupScreen, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(2f) + ) { ToastWithDate( - message = uiState.toastMessage, - modifier = Modifier - .align(Alignment.TopCenter) - .padding(horizontal = 20.dp, vertical = 16.dp) - .zIndex(2f) + message = uiState.toastMessage ) } @@ -529,7 +540,7 @@ fun GroupRoomRecruitScreenPreview() { recommendRooms = listOf( RecommendRoomResponse( roomId = 2, - roomImageUrl = "https://picsum.photos/300/400?rec1", + bookImageUrl = "https://picsum.photos/300/400?rec1", roomName = "📚 현대문학 깊이 탐구하기", memberCount = 12, recruitCount = 15, @@ -537,7 +548,7 @@ fun GroupRoomRecruitScreenPreview() { ), RecommendRoomResponse( roomId = 3, - roomImageUrl = "https://picsum.photos/300/400?rec2", + bookImageUrl = "https://picsum.photos/300/400?rec2", roomName = "✨ 철학 소설로 삶을 되돌아보기", memberCount = 8, recruitCount = 12, @@ -545,7 +556,7 @@ fun GroupRoomRecruitScreenPreview() { ), RecommendRoomResponse( roomId = 4, - roomImageUrl = "https://picsum.photos/300/400?rec3", + bookImageUrl = "https://picsum.photos/300/400?rec3", roomName = "🎭 인간 심리를 다룬 소설 읽기", memberCount = 15, recruitCount = 18, diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt index 2db56fb5..066326ce 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt @@ -66,6 +66,7 @@ fun GroupRoomUnlockScreen( viewModel.resetPasswordState() onSuccessNavigation() } + false -> { showError = true delay(1000) // 사용자에게 에러 메시지를 보여줄 시간 @@ -74,7 +75,8 @@ fun GroupRoomUnlockScreen( focusRequesters[0].requestFocus() viewModel.resetPasswordState() // ViewModel 상태 초기화 } - null -> { } + + null -> {} } } @@ -85,7 +87,7 @@ fun GroupRoomUnlockScreen( focusRequesters[0].requestFocus() keyboardController?.show() } - + // 화면 종료 시 리소스 정리 DisposableEffect(Unit) { onDispose { @@ -94,10 +96,7 @@ fun GroupRoomUnlockScreen( } } - Box( - modifier = Modifier - .fillMaxSize() - ) { + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.fillMaxSize() ) { @@ -133,25 +132,24 @@ fun GroupRoomUnlockScreen( onValueChange = { input -> if (input.length <= 1 && input.all { it.isDigit() }) { val newPassword = password.copyOf() + val wasEmpty = password[index].isEmpty() newPassword[index] = input password = newPassword + // 숫자가 입력되면 다음 칸으로 이동 if (input.isNotEmpty() && index < 3) { focusRequesters[index + 1].requestFocus() + } else if (input.isEmpty() && !wasEmpty && index > 0) { + focusRequesters[index - 1].requestFocus() } + } }, onBackspace = { - val newPassword = password.copyOf() - if (password[index].isNotEmpty()) { - // 현재 박스에 값이 있으면 현재 박스 지우기 - newPassword[index] = "" - password = newPassword - } else if (index > 0) { - // 현재 박스가 비어있으면 이전 박스로 이동하면서 지우기 - newPassword[index - 1] = "" - password = newPassword - focusRequesters[index - 1].requestFocus() + // 빈 박스에서 백스페이스 → 이전 박스로 이동 + if (index > 0) { + val prevIndex = index - 1 + focusRequesters[prevIndex].requestFocus() } }, borderColor = if (showError) colors.Red else Color.Transparent, diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index 8ca4dac4..42de8590 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -1,5 +1,9 @@ package com.texthip.thip.ui.group.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -54,7 +58,7 @@ fun GroupScreen( ) { // 화면 재진입 시 데이터 새로고침 LaunchedEffect(Unit) { - viewModel.refreshDataOnScreenEnter() + viewModel.resetToInitialState() } val uiState by viewModel.uiState.collectAsState() @@ -91,6 +95,11 @@ fun GroupContent( onHideToast: () -> Unit = {} ) { val scrollState = rememberScrollState() + + // 탭 전환 시 스크롤을 맨 위로 초기화 + LaunchedEffect(Unit) { + scrollState.scrollTo(0) + } Box( modifier = Modifier.fillMaxSize() @@ -165,13 +174,23 @@ fun GroupContent( ) // 토스트 팝업 - if (uiState.showToast) { + AnimatedVisibility( + visible = uiState.showToast, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(2f) + ) { ToastWithDate( - message = uiState.toastMessage, - modifier = Modifier - .align(Alignment.TopCenter) - .padding(horizontal = 20.dp, vertical = 16.dp) - .zIndex(2f) + message = uiState.toastMessage ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 5615c50d..854ebc4b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -206,4 +206,18 @@ class GroupViewModel @Inject constructor( refreshGroupData() } + fun resetToInitialState() { + // 장르 선택을 초기화하고 모든 데이터를 새로고침 + updateState { + it.copy( + selectedGenreIndex = 0, + roomMainList = it.roomMainList?.copy( + deadlineRoomList = emptyList(), + popularRoomList = emptyList() + ) + ) + } + refreshGroupData() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt index 17b63d74..16d4b6e7 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt @@ -2,13 +2,11 @@ package com.texthip.thip.ui.mypage.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt index 137f8cb6..10a75645 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt @@ -1,5 +1,9 @@ package com.texthip.thip.ui.mypage.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -40,20 +44,27 @@ fun NotificationScreen( LaunchedEffect(toastMessage) { if (toastMessage != null) { - delay(2000) + delay(3000) toastMessage = null } } Box(modifier = Modifier.fillMaxSize()) { - toastMessage?.let { message -> - Box( - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .align(Alignment.TopCenter) - .padding(horizontal = 15.dp, vertical = 15.dp), - contentAlignment = Alignment.TopCenter - ) { + AnimatedVisibility( + visible = toastMessage != null, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 15.dp, vertical = 15.dp) + .zIndex(1f) + ) { + toastMessage?.let { message -> ToastWithDate( message = stringResource( if (message == "push_on") R.string.push_on else R.string.push_off diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt index c214883c..4bcc60be 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -85,6 +86,12 @@ fun MyPageContent( onDeleteAccount: () -> Unit ) { val context = LocalContext.current + val listState = rememberLazyListState() + + // 탭 전환 시 스크롤을 맨 위로 초기화 + LaunchedEffect(Unit) { + listState.scrollToItem(0) + } Box( Modifier @@ -102,6 +109,7 @@ fun MyPageContent( rightIcon = painterResource(R.drawable.ic_plus) ) LazyColumn( + state = listState, modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(0.dp) ) { diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index e8121c84..7a721e31 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -13,6 +13,7 @@ import com.texthip.thip.ui.feed.screen.FeedWriteScreen import com.texthip.thip.ui.feed.screen.MySubscriptionScreen import com.texthip.thip.ui.feed.screen.OthersSubscriptionListScreen import com.texthip.thip.ui.feed.viewmodel.FeedWriteViewModel +import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToBookDetail import com.texthip.thip.ui.navigator.extensions.navigateToFeedComment import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite @@ -26,13 +27,18 @@ import com.texthip.thip.ui.navigator.routes.MainTabRoutes fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBack: () -> Unit) { composable { backStackEntry -> val resultFeedId = backStackEntry.savedStateHandle.get("feedId") + val refreshFeed = backStackEntry.savedStateHandle.get("refreshFeed") FeedScreen( navController = navController, resultFeedId = resultFeedId, + refreshFeed = refreshFeed, onResultConsumed = { backStackEntry.savedStateHandle.remove("feedId") }, + onRefreshConsumed = { + backStackEntry.savedStateHandle.remove("refreshFeed") + }, onNavigateToMySubscription = { navController.navigateToMySubscription() }, @@ -48,6 +54,9 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBac onNavigateToUserProfile = { userId -> navController.navigateToUserProfile(userId) }, + onNavigateToNotification = { + navController.navigateToAlarm() + }, onNavigateToOthersSubscription = { userId -> navController.navigateToOthersSubscription(userId) } @@ -102,6 +111,19 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBac bookImageUrl = route.bookImageUrl, recordContent = route.recordContent ) + } else if (route.isbn != null && + route.bookTitle != null && + route.bookAuthor != null + ) { + // 새 글 작성 모드: 책 정보만 있는 경우 (책 상세 페이지에서 온 경우) + viewModel.selectBook( + com.texthip.thip.ui.group.makeroom.mock.BookData( + title = route.bookTitle, + imageUrl = route.bookImageUrl ?: "", + author = route.bookAuthor, + isbn = route.isbn + ) + ) } } @@ -112,6 +134,8 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBac // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기 navController.getBackStackEntry(MainTabRoutes.Feed) .savedStateHandle["feedId"] = feedId + navController.getBackStackEntry(MainTabRoutes.Feed) + .savedStateHandle["refreshFeed"] = true navController.popBackStack(MainTabRoutes.Feed, inclusive = false) } ) @@ -147,6 +171,9 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController, navigateBac }, onNavigateToUserProfile = { userId -> navController.navigateToUserProfile(userId) + }, + onNavigateToBookDetail = { isbn -> + navController.navigateToBookDetail(isbn) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt index d1e08ad5..809dc468 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/SearchNavigation.kt @@ -7,6 +7,7 @@ import androidx.navigation.toRoute import com.texthip.thip.ui.navigator.extensions.navigateToBookDetail import com.texthip.thip.ui.navigator.extensions.navigateToBookGroup import com.texthip.thip.ui.navigator.extensions.navigateToFeedComment +import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoomWithBook import com.texthip.thip.ui.navigator.extensions.navigateToGroupRecruit import com.texthip.thip.ui.navigator.extensions.navigateToRegisterBook @@ -43,8 +44,13 @@ fun NavGraphBuilder.searchNavigation(navController: NavHostController) { onRecruitingGroupClick = { navController.navigateToBookGroup(isbn) }, - onWriteFeedClick = { - // TODO: 피드 작성 화면으로 이동 + onWriteFeedClick = { bookDetail -> + navController.navigateToFeedWrite( + isbn = bookDetail.isbn, + bookTitle = bookDetail.title, + bookAuthor = bookDetail.authorName, + bookImageUrl = bookDetail.imageUrl + ) }, onFeedClick = { feedId -> navController.navigateToFeedComment(feedId) diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt index b25d9c88..f8fad714 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt @@ -69,6 +69,7 @@ fun SearchActiveField( if (index < bookList.size - 1) { Spacer( modifier = Modifier + .padding(top = 12.dp) .fillMaxWidth() .height(1.dp) .background(colors.DarkGrey02) diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt index b6775d63..9f0b3b9d 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt @@ -96,6 +96,7 @@ fun SearchBookFilteredResult( if (index < bookList.size - 1) { Spacer( modifier = Modifier + .padding(top = 12.dp) .fillMaxWidth() .height(1.dp) .background(colors.DarkGrey02) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt index 41c14c10..1eb5e2c3 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.search.screen -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -38,6 +37,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -51,7 +51,6 @@ import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookDetailResponse import com.texthip.thip.ui.common.buttons.ActionMediumButton import com.texthip.thip.ui.common.modal.InfoPopup -import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.common.topappbar.GradationTopAppBar import com.texthip.thip.ui.mypage.component.SavedFeedCard import com.texthip.thip.ui.search.component.SearchFilterButton @@ -62,7 +61,6 @@ import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography import com.texthip.thip.utils.color.hexToColor -import kotlinx.coroutines.delay @Composable @@ -72,7 +70,7 @@ fun SearchBookDetailScreen( onLeftClick: () -> Unit = {}, onRightClick: () -> Unit = {}, onRecruitingGroupClick: () -> Unit = {}, - onWriteFeedClick: () -> Unit = {}, + onWriteFeedClick: (BookDetailResponse) -> Unit = {}, onFeedClick: (Long) -> Unit = {}, onBookmarkClick: (String, Boolean) -> Unit = { _, _ -> }, viewModel: BookDetailViewModel = hiltViewModel() @@ -91,7 +89,6 @@ fun SearchBookDetailScreen( bookDetail = uiState.bookDetail, uiState = uiState, onLeftClick = onLeftClick, - onRightClick = onRightClick, onRecruitingGroupClick = onRecruitingGroupClick, onWriteFeedClick = onWriteFeedClick, onFeedClick = onFeedClick, @@ -117,20 +114,24 @@ private fun SearchBookDetailScreenContent( bookDetail: BookDetailResponse? = null, uiState: BookDetailUiState? = null, onLeftClick: () -> Unit = {}, - onRightClick: () -> Unit = {}, onRecruitingGroupClick: () -> Unit = {}, - onWriteFeedClick: () -> Unit = {}, + onWriteFeedClick: (BookDetailResponse) -> Unit = {}, onFeedClick: (Long) -> Unit = {}, onBookmarkClick: (String, Boolean) -> Unit = { _, _ -> }, onSortChange: (String) -> Unit = {}, onLoadMore: () -> Unit = {} ) { - var isAlarmVisible by remember { mutableStateOf(true) } var isIntroductionPopupVisible by remember { mutableStateOf(false) } var isBookmarked by remember { mutableStateOf(bookDetail?.isSaved ?: false) } var isFilterDropdownVisible by remember { mutableStateOf(false) } var filterButtonPosition by remember { mutableStateOf(IntOffset.Zero) } val density = LocalDensity.current + val configuration = LocalConfiguration.current + + // 화면 크기에 따른 최대 높이 설정 (태블릿 대응) + val maxImageHeight = remember(configuration.screenHeightDp) { + (configuration.screenHeightDp * 0.6f).dp.coerceAtMost(620.dp) + } val filterOptions = listOf( stringResource(R.string.sort_like), stringResource(R.string.sort_latest) @@ -158,14 +159,6 @@ private fun SearchBookDetailScreenContent( } } - // 알림 5초간 노출 - LaunchedEffect(bookDetail) { - if (bookDetail != null) { - isAlarmVisible = true - delay(5000) - isAlarmVisible = false - } - } // 북마크 상태 동기화 LaunchedEffect(bookDetail?.isSaved) { @@ -228,7 +221,7 @@ private fun SearchBookDetailScreenContent( contentDescription = bookDetail.title, modifier = Modifier .fillMaxWidth() - .heightIn(min = 420.dp) + .heightIn(min = 420.dp, max = maxImageHeight) .blur(4.dp), contentScale = ContentScale.Crop, fallback = painterResource(R.drawable.img_book_cover_sample), @@ -241,9 +234,10 @@ private fun SearchBookDetailScreenContent( brush = Brush.verticalGradient( colors = listOf( Color.Transparent, - colors.Black.copy(alpha = 0.3f), - colors.Black.copy(alpha = 0.6f), - colors.Black.copy(alpha = 0.9f), + colors.Black.copy(alpha = 0.2f), + colors.Black.copy(alpha = 0.5f), + colors.Black.copy(alpha = 0.8f), + colors.Black.copy(alpha = 0.95f), colors.Black ), startY = 0f, @@ -330,7 +324,7 @@ private fun SearchBookDetailScreenContent( hasRightIcon = true, hasRightPlusIcon = true, modifier = Modifier.weight(1f), - onClick = onWriteFeedClick + onClick = { onWriteFeedClick(bookDetail) } ) Box( modifier = Modifier @@ -400,6 +394,24 @@ private fun SearchBookDetailScreenContent( } } + // 피드 섹션 전환을 위한 추가 그라데이션 + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(30.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + colors.Black, + colors.Black.copy(alpha = 0.95f), + colors.Black.copy(alpha = 0.9f) + ) + ) + ) + ) + } + // 피드 목록 (ViewModel에서 변환된 데이터 사용) val feedItems = uiState?.feedItems ?: emptyList() @@ -488,24 +500,13 @@ private fun SearchBookDetailScreenContent( } // TopAppBar 오버레이 (고정) - Column { - AnimatedVisibility(visible = isAlarmVisible) { - GradationTopAppBar( - isImageVisible = true, - count = bookDetail.readCount, - onLeftClick = onLeftClick, - onRightClick = {} - ) - } - AnimatedVisibility(visible = !isAlarmVisible) { - DefaultTopAppBar( - isRightIconVisible = true, - isTitleVisible = false, - onLeftClick = onLeftClick, - onRightClick = onRightClick - ) - } - } + GradationTopAppBar( + count = bookDetail.readCount, + autoHideCount = true, + countDisplayDurationMs = 5000L, + onLeftClick = onLeftClick, + onRightClick = {} + ) if (isIntroductionPopupVisible) { Box( diff --git a/app/src/main/res/drawable/ic_notice_no.xml b/app/src/main/res/drawable/ic_notice_no.xml new file mode 100644 index 00000000..b1f88d20 --- /dev/null +++ b/app/src/main/res/drawable/ic_notice_no.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e20ec44..3ca5c34e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,7 +27,7 @@ 한한강한강한강한ㅇㅇㄴㄴㅁ강 - + " 저" 내 피드에 추가 아니오