From e559c534337972b808eb641229b179fbac9e7c3b Mon Sep 17 00:00:00 2001 From: dgddgd314 Date: Mon, 27 Jul 2026 09:19:52 +0900 Subject: [PATCH] feat: able bookmark page, memo page on non-login env --- .../ui/navigation/HangshaNavHost.kt | 159 ++++++++++-------- .../view/bookmarks/GuestBookmarksViewModel.kt | 120 +++++++++++++ .../ui/view/guest/GuestMyPageViewModel.kt | 42 +---- .../ui/view/mypage/MyPageScreen.kt | 23 +-- 4 files changed, 209 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/com/example/hangsha_android/ui/view/bookmarks/GuestBookmarksViewModel.kt diff --git a/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt b/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt index ef295eb..a09080d 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import com.example.hangsha_android.BuildConfig import com.example.hangsha_android.ui.view.bookmarks.BookmarksScreen import com.example.hangsha_android.ui.view.bookmarks.BookmarksViewModel +import com.example.hangsha_android.ui.view.bookmarks.GuestBookmarksViewModel import com.example.hangsha_android.ui.view.calendar.CalendarFilterState import com.example.hangsha_android.ui.view.login.LoginScreen import com.example.hangsha_android.ui.view.login.LoginViewModel @@ -536,10 +537,35 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) { if (isLoggedIn) { SimplePageText("bookmark events") } else { - LoginRequiredScreen( - title = "북마크 목록은 로그인 후 이용할 수 있습니다.", - message = "게스트로 추가한 북마크는 캘린더 안에서 이 기기에만 저장됩니다.", - onLoginClick = { navController.navigateToLoginFromMain() } + val guestBookmarksViewModel: GuestBookmarksViewModel = hiltViewModel() + val guestBookmarksUiState by guestBookmarksViewModel.uiState.collectAsState() + + BookmarksScreen( + uiState = guestBookmarksUiState, + onNavigateBack = { + navController.navigate(BottomTab.Calendar.route) { + popUpTo(HangshaDestinations.Main.route) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + onEventClick = { eventId -> + navController.navigate(HangshaDestinations.EventDetail.createRoute(eventId)) + }, + onBookmarkClick = { eventId -> + guestBookmarksViewModel.removeBookmark(eventId) + }, + onRetryClick = { guestBookmarksViewModel.retry() }, + onLoadNextPage = { guestBookmarksViewModel.loadNextPage() }, + onScrollPositionChanged = { index, offset, itemId -> + guestBookmarksViewModel.saveScrollPosition( + firstVisibleItemIndex = index, + firstVisibleItemOffset = offset, + firstVisibleItemId = itemId + ) + } ) } } @@ -548,11 +574,27 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) { val isLoggedIn by authStateViewModel.isLoggedIn.collectAsState() if (!isLoggedIn) { - LoginRequiredScreen( - title = "내 북마크는 로그인 후 이용할 수 있습니다.", - message = "계정에 저장된 북마크 목록을 보려면 로그인해 주세요.", - onLoginClick = { navController.navigateToLoginFromMain() }, - onNavigateBack = { navController.popBackStack() } + val guestBookmarksViewModel: GuestBookmarksViewModel = hiltViewModel() + val guestBookmarksUiState by guestBookmarksViewModel.uiState.collectAsState() + + BookmarksScreen( + uiState = guestBookmarksUiState, + onNavigateBack = { navController.popBackStack() }, + onEventClick = { eventId -> + navController.navigate(HangshaDestinations.EventDetail.createRoute(eventId)) + }, + onBookmarkClick = { eventId -> + guestBookmarksViewModel.removeBookmark(eventId) + }, + onRetryClick = { guestBookmarksViewModel.retry() }, + onLoadNextPage = { guestBookmarksViewModel.loadNextPage() }, + onScrollPositionChanged = { index, offset, itemId -> + guestBookmarksViewModel.saveScrollPosition( + firstVisibleItemIndex = index, + firstVisibleItemOffset = offset, + firstVisibleItemId = itemId + ) + } ) } else { val bookmarksViewModel: BookmarksViewModel = hiltViewModel() @@ -611,57 +653,45 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) { } } composable(HangshaDestinations.MyMemos.route) { - val authStateViewModel: AuthStateViewModel = hiltViewModel() - val isLoggedIn by authStateViewModel.isLoggedIn.collectAsState() - - if (!isLoggedIn) { - LoginRequiredScreen( - title = "내 메모는 로그인 후 이용할 수 있습니다.", - message = "게스트 메모는 행사 상세 화면에서 이 기기에만 보관됩니다.", - onLoginClick = { navController.navigateToLoginFromMain() }, - onNavigateBack = { navController.popBackStack() } - ) - } else { - val myMemosViewModel: MyMemosViewModel = hiltViewModel() - val myMemosUiState by myMemosViewModel.uiState.collectAsState() - val context = LocalContext.current - val myMemosLifecycleOwner = LocalLifecycleOwner.current + val myMemosViewModel: MyMemosViewModel = hiltViewModel() + val myMemosUiState by myMemosViewModel.uiState.collectAsState() + val context = LocalContext.current + val myMemosLifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(myMemosUiState.toastMessage) { - val message = myMemosUiState.toastMessage ?: return@LaunchedEffect - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - myMemosViewModel.onToastMessageConsumed() - } + LaunchedEffect(myMemosUiState.toastMessage) { + val message = myMemosUiState.toastMessage ?: return@LaunchedEffect + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + myMemosViewModel.onToastMessageConsumed() + } - DisposableEffect(myMemosLifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - myMemosViewModel.loadMemos() - } - } - myMemosLifecycleOwner.lifecycle.addObserver(observer) - onDispose { - myMemosLifecycleOwner.lifecycle.removeObserver(observer) + DisposableEffect(myMemosLifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + myMemosViewModel.loadMemos() } } - - MyMemosScreen( - uiState = myMemosUiState, - onNavigateBack = { navController.popBackStack() }, - onMemoClick = { eventId -> - navController.navigate(HangshaDestinations.EventDetail.createRoute(eventId)) - }, - onDeleteMemoClick = { memoId -> myMemosViewModel.deleteMemo(memoId) }, - onStartEditMemo = { memo -> myMemosViewModel.startEditMemo(memo) }, - onEditContentChanged = { value -> myMemosViewModel.onEditContentChanged(value) }, - onStartAddingTag = { myMemosViewModel.startAddingTag() }, - onEditTagInputChanged = { value -> myMemosViewModel.onEditTagInputChanged(value) }, - onAddEditTag = { myMemosViewModel.addEditTag() }, - onRemoveEditTag = { tagName -> myMemosViewModel.removeEditTag(tagName) }, - onSaveEditedMemo = { myMemosViewModel.saveEditedMemo() }, - onRetryClick = { myMemosViewModel.loadMemos() } - ) + myMemosLifecycleOwner.lifecycle.addObserver(observer) + onDispose { + myMemosLifecycleOwner.lifecycle.removeObserver(observer) + } } + + MyMemosScreen( + uiState = myMemosUiState, + onNavigateBack = { navController.popBackStack() }, + onMemoClick = { eventId -> + navController.navigate(HangshaDestinations.EventDetail.createRoute(eventId)) + }, + onDeleteMemoClick = { memoId -> myMemosViewModel.deleteMemo(memoId) }, + onStartEditMemo = { memo -> myMemosViewModel.startEditMemo(memo) }, + onEditContentChanged = { value -> myMemosViewModel.onEditContentChanged(value) }, + onStartAddingTag = { myMemosViewModel.startAddingTag() }, + onEditTagInputChanged = { value -> myMemosViewModel.onEditTagInputChanged(value) }, + onAddEditTag = { myMemosViewModel.addEditTag() }, + onRemoveEditTag = { tagName -> myMemosViewModel.removeEditTag(tagName) }, + onSaveEditedMemo = { myMemosViewModel.saveEditedMemo() }, + onRetryClick = { myMemosViewModel.loadMemos() } + ) } composable( route = HangshaDestinations.InterestPriority.route, @@ -730,20 +760,10 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) { if (!isLoggedIn) { val guestMyPageViewModel: GuestMyPageViewModel = hiltViewModel() val guestMyPageUiState by guestMyPageViewModel.uiState.collectAsState() - val context = LocalContext.current - - LaunchedEffect(guestMyPageUiState.bugReportToastMessage) { - val message = guestMyPageUiState.bugReportToastMessage ?: return@LaunchedEffect - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - guestMyPageViewModel.onBugReportToastConsumed() - } GuestMyPageScreen( uiState = guestMyPageUiState, onLoginClick = { navController.navigateToLoginFromMain() }, - onInterestPriorityClick = { - navController.navigate(HangshaDestinations.InterestPriority.createRoute()) - }, onTimetableClick = { navController.navigate(BottomTab.Timetable.route) { popUpTo(HangshaDestinations.Main.route) { @@ -764,15 +784,6 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) { }, onMemoEventClick = { eventId -> navController.navigate(HangshaDestinations.EventDetail.createRoute(eventId)) - }, - onBugReportTitleChanged = { value -> - guestMyPageViewModel.onBugReportTitleChanged(value) - }, - onBugReportContentChanged = { value -> - guestMyPageViewModel.onBugReportContentChanged(value) - }, - onSubmitBugReportClick = { - guestMyPageViewModel.submitBugReport() } ) } else { diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/bookmarks/GuestBookmarksViewModel.kt b/app/src/main/java/com/example/hangsha_android/ui/view/bookmarks/GuestBookmarksViewModel.kt new file mode 100644 index 0000000..98fb033 --- /dev/null +++ b/app/src/main/java/com/example/hangsha_android/ui/view/bookmarks/GuestBookmarksViewModel.kt @@ -0,0 +1,120 @@ +package com.example.hangsha_android.ui.view.bookmarks + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.hangsha_android.data.local.BookmarksLocalDataSource +import com.example.hangsha_android.data.local.StoredGuestBookmarkSnapshot +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class GuestBookmarksViewModel @Inject constructor( + private val bookmarksLocalDataSource: BookmarksLocalDataSource +) : ViewModel() { + private val _uiState = MutableStateFlow(BookmarksUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + combine( + bookmarksLocalDataSource.bookmarkedEventIds(isLoggedIn = false), + bookmarksLocalDataSource.guestBookmarkSnapshots + ) { bookmarkedEventIds, bookmarkSnapshots -> + val current = _uiState.value + current.copy( + items = bookmarkSnapshots.toBookmarkedEventItems(bookmarkedEventIds), + isInitialLoading = false, + isLoadingNextPage = false, + hasMore = false, + nextPage = 1, + errorMessage = null + ) + }.collect { nextState -> + _uiState.update { nextState } + } + } + } + + fun removeBookmark(eventId: Long) { + viewModelScope.launch { + bookmarksLocalDataSource.setBookmarked( + eventId = eventId, + isBookmarked = false, + isLoggedIn = false + ) + } + } + + fun retry() = Unit + + fun loadNextPage() = Unit + + fun saveScrollPosition( + firstVisibleItemIndex: Int, + firstVisibleItemOffset: Int, + firstVisibleItemId: Long? + ) { + _uiState.update { + it.copy( + savedScrollIndex = firstVisibleItemIndex, + savedScrollOffset = firstVisibleItemOffset, + savedAnchorItemId = firstVisibleItemId + ) + } + } +} + +private fun List.toBookmarkedEventItems( + bookmarkedEventIds: Set +): List { + val snapshotsByEventId = associateBy { it.eventId } + return bookmarkedEventIds.map { eventId -> + snapshotsByEventId[eventId]?.toBookmarkedEventItem() + ?: BookmarkedEventItem( + id = eventId, + title = "Event #$eventId", + imageUrl = null, + eventTypeId = 0L, + statusId = 0L, + dDayLabel = "-", + applyPeriodDisplay = "-", + organization = null, + isBookmarked = true + ) + }.sortedByDescending { item -> snapshotsByEventId[item.id]?.updatedAt.orEmpty() } +} + +private fun StoredGuestBookmarkSnapshot.toBookmarkedEventItem(): BookmarkedEventItem { + return BookmarkedEventItem( + id = eventId, + title = title, + imageUrl = imageUrl, + eventTypeId = eventTypeId, + statusId = 0L, + dDayLabel = dDayLabel.toDdayLabel(), + applyPeriodDisplay = applyPeriodDisplay ?: "-", + organization = organization, + isBookmarked = true + ) +} + +private fun String?.toDdayLabel(): String { + val normalized = this?.trim().orEmpty() + .removePrefix("Apply") + .removePrefix("지원") + .trim() + if (normalized.isBlank()) { + return "-" + } + return when { + normalized.equals("D-DAY", ignoreCase = true) -> "D-day" + normalized.startsWith("D") -> normalized + else -> normalized + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/guest/GuestMyPageViewModel.kt b/app/src/main/java/com/example/hangsha_android/ui/view/guest/GuestMyPageViewModel.kt index 38dbad9..6cfccfd 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/view/guest/GuestMyPageViewModel.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/view/guest/GuestMyPageViewModel.kt @@ -30,15 +30,10 @@ class GuestMyPageViewModel @Inject constructor( bookmarksLocalDataSource.guestBookmarkSnapshots, guestMemosLocalDataSource.memos ) { bookmarkedEventIds, bookmarkSnapshots, memos -> - val current = _uiState.value GuestMyPageUiState( bookmarkItems = bookmarkSnapshots.toGuestBookmarkPreviewItems(bookmarkedEventIds), bookmarkCount = bookmarkedEventIds.size, - memoItems = memos.map { it.toGuestMemoPreviewItem() }, - bugReportTitle = current.bugReportTitle, - bugReportContent = current.bugReportContent, - isSubmittingBugReport = current.isSubmittingBugReport, - bugReportToastMessage = current.bugReportToastMessage + memoItems = memos.map { it.toGuestMemoPreviewItem() } ) }.collect { nextState -> _uiState.update { nextState } @@ -46,45 +41,12 @@ class GuestMyPageViewModel @Inject constructor( } } - fun onBugReportTitleChanged(value: String) { - _uiState.update { - it.copy( - bugReportTitle = value, - bugReportToastMessage = null - ) - } - } - - fun onBugReportContentChanged(value: String) { - _uiState.update { - it.copy( - bugReportContent = value, - bugReportToastMessage = null - ) - } - } - - fun submitBugReport() { - _uiState.update { - it.copy(bugReportToastMessage = "Please log in to submit a bug report.") - } - } - - fun onBugReportToastConsumed() { - _uiState.update { - it.copy(bugReportToastMessage = null) - } - } } data class GuestMyPageUiState( val bookmarkItems: List = emptyList(), val bookmarkCount: Int = 0, - val memoItems: List = emptyList(), - val bugReportTitle: String = "", - val bugReportContent: String = "", - val isSubmittingBugReport: Boolean = false, - val bugReportToastMessage: String? = null + val memoItems: List = emptyList() ) { val memoCount: Int get() = memoItems.size diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/mypage/MyPageScreen.kt b/app/src/main/java/com/example/hangsha_android/ui/view/mypage/MyPageScreen.kt index 558da03..09a96f4 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/view/mypage/MyPageScreen.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/view/mypage/MyPageScreen.kt @@ -253,15 +253,11 @@ fun MyPageScreen( fun GuestMyPageScreen( uiState: GuestMyPageUiState, onLoginClick: () -> Unit, - onInterestPriorityClick: () -> Unit, onTimetableClick: () -> Unit, onBookmarksClick: () -> Unit, onBookmarkedEventClick: (Long) -> Unit, onMemoListClick: () -> Unit, - onMemoEventClick: (Long) -> Unit, - onBugReportTitleChanged: (String) -> Unit, - onBugReportContentChanged: (String) -> Unit, - onSubmitBugReportClick: () -> Unit + onMemoEventClick: (Long) -> Unit ) { Box( modifier = Modifier @@ -280,11 +276,6 @@ fun GuestMyPageScreen( item { GuestProfileLoginPrompt(onLoginClick = onLoginClick) Spacer(modifier = Modifier.height(20.dp)) - PrioritySection( - interests = emptyList(), - onClick = onInterestPriorityClick - ) - Spacer(modifier = Modifier.height(8.dp)) TimetableRegistrationRow(onClick = onTimetableClick) Spacer(modifier = Modifier.height(29.dp)) BookmarksPreviewSection( @@ -318,17 +309,7 @@ fun GuestMyPageScreen( onHeaderClick = onMemoListClick, onMemoClick = onMemoEventClick ) - Spacer(modifier = Modifier.height(24.dp)) - Divider() - Spacer(modifier = Modifier.height(17.dp)) - BugReportSection( - title = uiState.bugReportTitle, - content = uiState.bugReportContent, - isSubmitting = uiState.isSubmittingBugReport, - onTitleChanged = onBugReportTitleChanged, - onContentChanged = onBugReportContentChanged, - onSubmitClick = onSubmitBugReportClick - ) + } } }