Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +35,9 @@ val viewModelModule = module {
getRecipeDetailsUseCase = get(),
getRecipeProcedureUseCase = get(),
copyLinkUseCase = get(),
addBookmarkUseCase = get(),
removeBookmarkUseCase = get(),
getBookmarkedRecipeIdsUseCase = get(),
)
}
viewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,6 +14,8 @@ class RecipeRepositoryImpl(
override suspend fun findRecipe(id: Long): Result<Recipe, String> {
try {
return Result.Success(dataSource.getRecipe(id)!!.toModel())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
return Result.Error("error : findRecipe($id) 실패")
}
Expand All @@ -25,6 +28,8 @@ class RecipeRepositoryImpl(
?.filterNotNull()
?.map { it.toModel() }
?: emptyList())
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
return Result.Error("error : findRecipes() 실패")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Set<Long>, 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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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()
}
}

// 카테고리 선택에 따라 선택된 레시피 리스트 업데이트
Expand Down Expand Up @@ -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)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ data class IngredientState(
),
val tabIndex: Int = 0,
val procedures: List<String> = emptyList(),
val isBookmarked: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -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("북마크 로드 실패")
}
}
}

Expand All @@ -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("북마크 토글 실패")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ fun SavedRecipesRoot(
) {
val state by viewModel.state.collectAsStateWithLifecycle()

// 화면 진입 시 데이터 갱신
LaunchedEffect(Unit) {
viewModel.loadRecipes()
}

val listState = rememberLazyListState() // 스크롤 위치 추적

LaunchedEffect(listState) {
Expand All @@ -35,6 +40,7 @@ fun SavedRecipesRoot(
state = state,
listState = listState,
onCardClick = { recipeId -> onCardClick(recipeId) },
onBookmarkClick = { recipeId -> viewModel.removeBookmark(recipeId) },
)
}

Expand Down
Loading
Loading