From ac103fa864721da938816f872955831b2060e8fb Mon Sep 17 00:00:00 2001 From: Neouul Date: Fri, 2 Jan 2026 03:30:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=97=B0=EC=8A=B5=EB=AC=B8=EC=A0=9C=201=20-=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC=20=EA=B8=B0=EB=8A=A5=20Room=20DB=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/di/UseCaseModule.kt | 6 + .../core/di/ViewModelModule.kt | 14 ++- .../data/repository/RecipeRepositoryImpl.kt | 5 + .../domain/usecase/AddBookmarkUseCase.kt | 11 ++ .../usecase/GetBookmarkedRecipeIdsUseCase.kt | 20 +++ .../domain/usecase/RemoveBookmarkUseCase.kt | 11 ++ .../presentation/screen/home/HomeRoot.kt | 5 + .../presentation/screen/home/HomeViewModel.kt | 118 ++++++++++++------ .../screen/ingredient/IngredientState.kt | 1 + .../screen/ingredient/IngredientViewModel.kt | 40 ++++++ .../screen/saved_recipe/SavedRecipesRoot.kt | 6 + .../saved_recipe/SavedRecipesViewModel.kt | 27 ++++ 12 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/AddBookmarkUseCase.kt create mode 100644 app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetBookmarkedRecipeIdsUseCase.kt create mode 100644 app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/RemoveBookmarkUseCase.kt diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt index 19e530abb..fcaefbc69 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt @@ -1,10 +1,13 @@ package com.survivalcoding.gangnam2kiandroidstudy.core.di +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.AddBookmarkUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.CopyLinkUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetBookmarkedRecipeIdsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeProcedureUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetSavedRecipesUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.RemoveBookmarkUseCase import org.koin.dsl.module val useCaseModule = module { @@ -13,4 +16,7 @@ val useCaseModule = module { single { GetRecipesUseCase(get()) } single { GetRecipeDetailsUseCase(get()) } single { GetRecipeProcedureUseCase(get()) } + single { AddBookmarkUseCase(get()) } + single { RemoveBookmarkUseCase(get()) } + single { GetBookmarkedRecipeIdsUseCase(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt index db07a79d5..be66d89dc 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt @@ -9,11 +9,20 @@ import org.koin.core.module.dsl.viewModel import org.koin.dsl.module val viewModelModule = module { - viewModel { HomeViewModel(getRecipesUseCase = get()) } + viewModel { + HomeViewModel( + getRecipesUseCase = get(), + getBookmarkedRecipeIdsUseCase = get(), + addBookmarkUseCase = get(), + removeBookmarkUseCase = get(), + ) + } viewModel { SavedRecipesViewModel( getSavedRecipesUseCase = get(), getRecipeDetailsUseCase = get(), + addBookmarkUseCase = get(), + removeBookmarkUseCase = get(), ) } viewModel { @@ -26,6 +35,9 @@ val viewModelModule = module { getRecipeDetailsUseCase = get(), getRecipeProcedureUseCase = get(), copyLinkUseCase = get(), + addBookmarkUseCase = get(), + removeBookmarkUseCase = get(), + getBookmarkedRecipeIdsUseCase = get(), ) } viewModel { diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/data/repository/RecipeRepositoryImpl.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/data/repository/RecipeRepositoryImpl.kt index 85709727d..ee34c2f34 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/data/repository/RecipeRepositoryImpl.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/data/repository/RecipeRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.survivalcoding.gangnam2kiandroidstudy.data.mapper.toModel import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Recipe import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import kotlin.coroutines.cancellation.CancellationException class RecipeRepositoryImpl( private val dataSource: RecipeDataSource @@ -13,6 +14,8 @@ class RecipeRepositoryImpl( override suspend fun findRecipe(id: Long): Result { try { return Result.Success(dataSource.getRecipe(id)!!.toModel()) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { return Result.Error("error : findRecipe($id) 실패") } @@ -25,6 +28,8 @@ class RecipeRepositoryImpl( ?.filterNotNull() ?.map { it.toModel() } ?: emptyList()) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { return Result.Error("error : findRecipes() 실패") } diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/AddBookmarkUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/AddBookmarkUseCase.kt new file mode 100644 index 000000000..d75de036d --- /dev/null +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/AddBookmarkUseCase.kt @@ -0,0 +1,11 @@ +package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase + +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.BookmarkRepository + +class AddBookmarkUseCase( + private val bookmarkRepository: BookmarkRepository +) { + suspend operator fun invoke(recipeId: Long) { + bookmarkRepository.addBookmark(recipeId) + } +} diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetBookmarkedRecipeIdsUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetBookmarkedRecipeIdsUseCase.kt new file mode 100644 index 000000000..573331d97 --- /dev/null +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetBookmarkedRecipeIdsUseCase.kt @@ -0,0 +1,20 @@ +package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase + +import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.BookmarkRepository +import kotlin.coroutines.cancellation.CancellationException + +class GetBookmarkedRecipeIdsUseCase( + private val bookmarkRepository: BookmarkRepository +) { + suspend fun execute(): Result, String> { + return try { + val ids = bookmarkRepository.getBookmarkedRecipeIds() + Result.Success(ids) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.Error(e.message ?: "Failed to load bookmarks") + } + } +} diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/RemoveBookmarkUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/RemoveBookmarkUseCase.kt new file mode 100644 index 000000000..7dc8b50d8 --- /dev/null +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/RemoveBookmarkUseCase.kt @@ -0,0 +1,11 @@ +package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase + +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.BookmarkRepository + +class RemoveBookmarkUseCase( + private val bookmarkRepository: BookmarkRepository +) { + suspend operator fun invoke(recipeId: Long) { + bookmarkRepository.removeBookmark(recipeId) + } +} diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeRoot.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeRoot.kt index c09221591..38a07dac4 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeRoot.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeRoot.kt @@ -15,6 +15,11 @@ fun HomeRoot( ) { val state by viewModel.state.collectAsStateWithLifecycle() + // 화면 진입 시 데이터 갱신 + LaunchedEffect(Unit) { + viewModel.loadData() + } + LaunchedEffect(viewModel.event) { viewModel.event.collect { event -> when (event) { diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt index 57019878e..8b6da40dc 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt @@ -6,7 +6,10 @@ import com.survivalcoding.gangnam2kiandroidstudy.core.Result import com.survivalcoding.gangnam2kiandroidstudy.domain.model.RecipeCategory import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toCategory import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toCategory +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.AddBookmarkUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetBookmarkedRecipeIdsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.RemoveBookmarkUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -18,6 +21,9 @@ import kotlinx.coroutines.launch class HomeViewModel( private val getRecipesUseCase: GetRecipesUseCase, + private val getBookmarkedRecipeIdsUseCase: GetBookmarkedRecipeIdsUseCase, + private val addBookmarkUseCase: AddBookmarkUseCase, + private val removeBookmarkUseCase: RemoveBookmarkUseCase, ) : ViewModel() { private val _state = MutableStateFlow(HomeState()) @@ -64,7 +70,47 @@ class HomeViewModel( init { println("MainViewModel init") - loadRecipes() + loadData() + } + + fun loadData() { + loadJob?.cancel() + loadJob = viewModelScope.launch { + _state.update { it.copy(isLoading = true, isError = false) } + + // Load Recipes + val recipesResult = getRecipesUseCase.execute() + + // Load Bookmarks + val bookmarksResult = getBookmarkedRecipeIdsUseCase.execute() + + if (recipesResult is Result.Success && bookmarksResult is Result.Success) { + val all = recipesResult.data + val savedIds = bookmarksResult.data + + _state.update { currentState -> + currentState.copy( + allRecipes = all, + savedRecipeIds = savedIds, + selectedRecipes = if (currentState.selectedCategory.toCategory() == RecipeCategory.ALL) { + all + } else if (currentState.selectedCategory.toCategory() == RecipeCategory.NONE) { + emptyList() + } else { + all.filter { recipe -> + recipe.category == currentState.selectedCategory.toCategory() + } + }, + isLoading = false, + ) + } + } else { + println("에러 처리") + _state.update { it.copy(isLoading = false, isError = true) } + } + + getNewRecipesTop5() + } } // 카테고리 선택에 따라 선택된 레시피 리스트 업데이트 @@ -94,50 +140,42 @@ class HomeViewModel( // 모든 레시피 읽어오기 // race condition 방지 private var loadJob: Job? = null - private fun loadRecipes() { - loadJob?.cancel() - loadJob = viewModelScope.launch { - _state.update { it.copy(isLoading = true, isError = false) } - - when (val response = getRecipesUseCase.execute()) { - is Result.Success -> _state.update { currentState -> - val all = response.data - - currentState.copy( - allRecipes = all, - selectedRecipes = if (currentState.selectedCategory.toCategory() == RecipeCategory.ALL) { - all - } else if (currentState.selectedCategory.toCategory() == RecipeCategory.NONE) { - emptyList() - } else { - all.filter { recipe -> - recipe.category == currentState.selectedCategory.toCategory() - } - }, - isLoading = false, - ) - } - - is Result.Error -> { - println("에러 처리") - _state.update { it.copy(isLoading = false, isError = true) } - } - } - - getNewRecipesTop5() - } - } // 레시피 저장 private fun toggleBookmark(recipeId: Long) { - _state.update { state -> - val newBookmarks = if (recipeId in state.savedRecipeIds) { - state.savedRecipeIds - recipeId // 제거 - } else { - state.savedRecipeIds + recipeId // 추가 + viewModelScope.launch { + val isBookmarked = recipeId in state.value.savedRecipeIds + + // UI 낙관적 업데이트 + _state.update { state -> + val newBookmarks = if (isBookmarked) { + state.savedRecipeIds - recipeId + } else { + state.savedRecipeIds + recipeId + } + state.copy(savedRecipeIds = newBookmarks) } - state.copy(savedRecipeIds = newBookmarks) + // 실제 DB 업데이트 + try { + if (isBookmarked) { + removeBookmarkUseCase(recipeId) + } else { + addBookmarkUseCase(recipeId) + } + } catch (e: Exception) { + // 실패 시 롤백 (선택 사항, 여기서는 간단히 에러 로그만) + println("북마크 토글 실패: $e") + // 실패했을 경우 다시 원래대로 돌려놓는 로직이 있으면 좋음 + _state.update { state -> + val newBookmarks = if (isBookmarked) { + state.savedRecipeIds + recipeId // 원래대로 복구 + } else { + state.savedRecipeIds - recipeId // 원래대로 복구 + } + state.copy(savedRecipeIds = newBookmarks) + } + } } } diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientState.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientState.kt index 34938a3da..50089d206 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientState.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientState.kt @@ -16,4 +16,5 @@ data class IngredientState( ), val tabIndex: Int = 0, val procedures: List = emptyList(), + val isBookmarked: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt index 082152dcc..6039d3094 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt @@ -3,9 +3,12 @@ package com.survivalcoding.gangnam2kiandroidstudy.presentation.screen.ingredient import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.AddBookmarkUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.CopyLinkUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetBookmarkedRecipeIdsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeProcedureUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.RemoveBookmarkUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -16,6 +19,9 @@ class IngredientViewModel( private val getRecipeDetailsUseCase: GetRecipeDetailsUseCase, private val getRecipeProcedureUseCase: GetRecipeProcedureUseCase, private val copyLinkUseCase: CopyLinkUseCase, + private val addBookmarkUseCase: AddBookmarkUseCase, + private val removeBookmarkUseCase: RemoveBookmarkUseCase, + private val getBookmarkedRecipeIdsUseCase: GetBookmarkedRecipeIdsUseCase, ) : ViewModel() { private val _state = MutableStateFlow(IngredientState()) @@ -27,11 +33,21 @@ class IngredientViewModel( fun loadRecipeDetail(recipeId: Long) { viewModelScope.launch { + // 레시피 상세 정보 로드 when (val response = getRecipeDetailsUseCase.execute(recipeId)) { is Result.Success -> _state.update { it.copy(recipe = response.data) } is Result.Error -> println("에러 처리") } + + // 북마크 상태 확인 + when (val bookmarksResult = getBookmarkedRecipeIdsUseCase.execute()) { + is Result.Success -> { + val isBookmarked = recipeId in bookmarksResult.data + _state.update { it.copy(isBookmarked = isBookmarked) } + } + is Result.Error -> println("북마크 로드 실패") + } } } @@ -53,4 +69,28 @@ class IngredientViewModel( fun copyLink(link: String) { copyLinkUseCase(link) } + + fun toggleBookmark() { + val recipeId = state.value.recipe.id + if (recipeId == 0L) return + + viewModelScope.launch { + val isBookmarked = state.value.isBookmarked + + // 낙관적 업데이트 + _state.update { it.copy(isBookmarked = !isBookmarked) } + + try { + if (isBookmarked) { + removeBookmarkUseCase(recipeId) + } else { + addBookmarkUseCase(recipeId) + } + } catch (e: Exception) { + // 롤백 + _state.update { it.copy(isBookmarked = isBookmarked) } + println("북마크 토글 실패") + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesRoot.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesRoot.kt index 5255432a9..2251eb83f 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesRoot.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesRoot.kt @@ -18,6 +18,11 @@ fun SavedRecipesRoot( ) { val state by viewModel.state.collectAsStateWithLifecycle() + // 화면 진입 시 데이터 갱신 + LaunchedEffect(Unit) { + viewModel.loadRecipes() + } + val listState = rememberLazyListState() // 스크롤 위치 추적 LaunchedEffect(listState) { @@ -35,6 +40,7 @@ fun SavedRecipesRoot( state = state, listState = listState, onCardClick = { recipeId -> onCardClick(recipeId) }, + onBookmarkClick = { recipeId -> viewModel.removeBookmark(recipeId) }, ) } diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt index 198c162ca..1f6ac5ed2 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt @@ -3,8 +3,10 @@ package com.survivalcoding.gangnam2kiandroidstudy.presentation.screen.saved_reci import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.AddBookmarkUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetSavedRecipesUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.RemoveBookmarkUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,6 +16,8 @@ import kotlinx.coroutines.launch class SavedRecipesViewModel( private val getSavedRecipesUseCase: GetSavedRecipesUseCase, private val getRecipeDetailsUseCase: GetRecipeDetailsUseCase, + private val addBookmarkUseCase: AddBookmarkUseCase, + private val removeBookmarkUseCase: RemoveBookmarkUseCase, ) : ViewModel() { private val _state = MutableStateFlow(SavedRecipesState()) @@ -37,6 +41,15 @@ class SavedRecipesViewModel( } suspend fun saveNewRecipe(id: Long) { + // DB에 저장 + try { + addBookmarkUseCase(id) + } catch (e: Exception) { + println("저장 실패: $e") + return + } + + // UI 업데이트 when (val response = getRecipeDetailsUseCase.execute(id)) { is Result.Success -> _state.update { currentState -> // 이미 저장된 레시피면 그대로 @@ -52,6 +65,20 @@ class SavedRecipesViewModel( is Result.Error -> println("에러 처리") } } + + fun removeBookmark(recipeId: Long) { + viewModelScope.launch { + try { + removeBookmarkUseCase(recipeId) + // 목록에서 제거 + _state.update { state -> + state.copy(savedRecipes = state.savedRecipes.filter { it.id != recipeId }) + } + } catch (e: Exception) { + println("삭제 실패: $e") + } + } + } // 파괴될 때 override fun onCleared() {