diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt index 344c29d..5d5e461 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt @@ -50,7 +50,7 @@ fun QuestionScreen( uiState.searchQuery, uiState.selectedCategories, uiState.showSolvedOnly, - uiState.showFavoritesOnly, + uiState.showLikedOnly, ) { if (listState.firstVisibleItemIndex > 0) { listState.scrollToItem(0) @@ -79,9 +79,9 @@ fun QuestionScreen( onCategoryToggle = viewModel::onCategoryToggle, onToggleCategoryFilters = viewModel::onToggleCategoryFilters, onSolvedFilterToggle = viewModel::onSolvedFilterToggle, - onFavoritesFilterToggle = viewModel::onFavoritesFilterToggle, + onLikedFilterToggle = viewModel::onLikedFilterToggle, onQuestionClick = {}, - onFavoriteToggle = viewModel::onFavoriteToggle, + onLikeToggle = viewModel::onLikeToggle, ) } @@ -93,9 +93,9 @@ private fun QuestionScreenContent( onCategoryToggle: (Category) -> Unit, onToggleCategoryFilters: () -> Unit, onSolvedFilterToggle: () -> Unit, - onFavoritesFilterToggle: () -> Unit, + onLikedFilterToggle: () -> Unit, onQuestionClick: (Long) -> Unit, - onFavoriteToggle: (Long) -> Unit, + onLikeToggle: (Long) -> Unit, ) { Column( modifier = @@ -115,10 +115,10 @@ private fun QuestionScreenContent( QuestionFilterChips( selectedCategories = uiState.selectedCategories, showSolvedOnly = uiState.showSolvedOnly, - showFavoritesOnly = uiState.showFavoritesOnly, + showLikedOnly = uiState.showLikedOnly, onToggleCategoryFilters = onToggleCategoryFilters, onSolvedFilterToggle = onSolvedFilterToggle, - onFavoritesFilterToggle = onFavoritesFilterToggle, + onLikedFilterToggle = onLikedFilterToggle, ) Spacer(modifier = Modifier.height(Dimen.spacingMd)) @@ -156,7 +156,7 @@ private fun QuestionScreenContent( QuestionList( questions = uiState.filteredQuestions, onQuestionClick = onQuestionClick, - onFavoriteToggle = onFavoriteToggle, + onLikeToggle = onLikeToggle, listState = listState, ) } diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt index d7718cd..ae77cd9 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt @@ -27,10 +27,10 @@ import org.jetbrains.compose.resources.stringResource fun QuestionFilterChips( selectedCategories: ImmutableSet, showSolvedOnly: Boolean, - showFavoritesOnly: Boolean, + showLikedOnly: Boolean, onToggleCategoryFilters: () -> Unit, onSolvedFilterToggle: () -> Unit, - onFavoritesFilterToggle: () -> Unit, + onLikedFilterToggle: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -63,8 +63,8 @@ fun QuestionFilterChips( CategoryFilterChip( text = stringResource(Res.string.question_filter_favorites), - selected = showFavoritesOnly, - onClick = onFavoritesFilterToggle, + selected = showLikedOnly, + onClick = onLikedFilterToggle, leadingIcon = { Icon( imageVector = Icons.Filled.Star, @@ -82,10 +82,10 @@ private fun QuestionFilterChipsPreview() { QuestionFilterChips( selectedCategories = persistentSetOf(), showSolvedOnly = false, - showFavoritesOnly = false, + showLikedOnly = false, onToggleCategoryFilters = {}, onSolvedFilterToggle = {}, - onFavoritesFilterToggle = {}, + onLikedFilterToggle = {}, ) } } @@ -97,10 +97,10 @@ private fun QuestionFilterChipsSelectedPreview() { QuestionFilterChips( selectedCategories = persistentSetOf(Category.Kotlin, Category.Android), showSolvedOnly = true, - showFavoritesOnly = true, + showLikedOnly = true, onToggleCategoryFilters = {}, onSolvedFilterToggle = {}, - onFavoritesFilterToggle = {}, + onLikedFilterToggle = {}, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt index 2065a99..3badd3e 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt @@ -22,7 +22,7 @@ import kotlin.time.Instant fun QuestionList( questions: ImmutableList, onQuestionClick: (Long) -> Unit, - onFavoriteToggle: (Long) -> Unit, + onLikeToggle: (Long) -> Unit, modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), ) { @@ -40,9 +40,9 @@ fun QuestionList( title = question.title, category = question.category, isSolved = question.isSolved, - isFavorite = question.isFavorite, + isLiked = question.isLiked, onClick = { onQuestionClick(question.id) }, - onFavoriteClick = { onFavoriteToggle(question.id) }, + onLikeClick = { onLikeToggle(question.id) }, ) } } @@ -55,7 +55,7 @@ private fun QuestionListEmptyPreview() { QuestionList( questions = persistentListOf(), onQuestionClick = {}, - onFavoriteToggle = {}, + onLikeToggle = {}, ) } } @@ -75,11 +75,11 @@ private fun QuestionListSinglePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = false, - isFavorite = false, + isLiked = false, ), ), onQuestionClick = {}, - onFavoriteToggle = {}, + onLikeToggle = {}, ) } } @@ -99,7 +99,7 @@ private fun QuestionListMultiplePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = true, - isFavorite = true, + isLiked = true, ), Question( id = 2L, @@ -109,7 +109,7 @@ private fun QuestionListMultiplePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = false, - isFavorite = true, + isLiked = true, ), Question( id = 3L, @@ -119,7 +119,7 @@ private fun QuestionListMultiplePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = true, - isFavorite = false, + isLiked = false, ), Question( id = 4L, @@ -129,7 +129,7 @@ private fun QuestionListMultiplePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = false, - isFavorite = false, + isLiked = false, ), Question( id = 5L, @@ -139,11 +139,11 @@ private fun QuestionListMultiplePreview() { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = false, - isFavorite = false, + isLiked = false, ), ), onQuestionClick = {}, - onFavoriteToggle = {}, + onLikeToggle = {}, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt index 1fcf30b..9fc4010 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt @@ -23,7 +23,7 @@ data class QuestionUiState( val selectedCategories: ImmutableSet get() = filter.categories.toSet().toImmutableSet() val showSolvedOnly: Boolean get() = filter.solved - val showFavoritesOnly: Boolean get() = filter.liked + val showLikedOnly: Boolean get() = filter.liked val filteredQuestions: ImmutableList get() { @@ -48,9 +48,21 @@ data class QuestionUiState( fun clearSolvedFilter(): QuestionUiState = copy(filter = filter.clearSolvedFilter()) - fun applyFavoritesFilter(): QuestionUiState = copy(filter = filter.applyFavoritesFilter()) + fun applyLikedFilter(): QuestionUiState = copy(filter = filter.applyLikedFilter()) - fun clearFavoritesFilter(): QuestionUiState = copy(filter = filter.clearFavoritesFilter()) + fun clearLikedFilter(): QuestionUiState = copy(filter = filter.clearLikedFilter()) + + fun toggleQuestionLike(questionId: Long): QuestionUiState { + val updatedList = + allQuestions.toList().map { question -> + if (question.id == questionId) { + question.copy(isLiked = !question.isLiked) + } else { + question + } + } + return copy(allQuestions = Questions(updatedList)) + } fun loading(isLoading: Boolean): QuestionUiState = copy(isLoading = isLoading) diff --git a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt index 62a8590..72e0fbe 100644 --- a/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt @@ -59,12 +59,12 @@ class QuestionViewModel( applyFilter() } - fun onFavoritesFilterToggle() { + fun onLikedFilterToggle() { _uiState.update { - if (it.showFavoritesOnly) { - it.clearFavoritesFilter() + if (it.showLikedOnly) { + it.clearLikedFilter() } else { - it.applyFavoritesFilter() + it.applyLikedFilter() } } applyFilter() @@ -82,8 +82,22 @@ class QuestionViewModel( } } - fun onFavoriteToggle(questionId: Long) { - // TODO: Implement favorite toggle + fun onLikeToggle(questionId: Long) { + viewModelScope.launch { + val currentQuestion = _uiState.value.filteredQuestions.find { it.id == questionId } ?: return@launch + val isCurrentlyLiked = currentQuestion.isLiked + + _uiState.update { it.toggleQuestionLike(questionId) } + + questionRepository + .toggleQuestionLike(questionId, isCurrentlyLiked) + .onFailure { + _uiState.update { state -> + state.toggleQuestionLike(questionId) + } + sendUiEvent(QuestionUiEvent.ShowError) + } + } } private fun loadQuestions() { diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt index 48ffd08..9af92c0 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt @@ -1,45 +1,50 @@ package com.peto.droidmorning.data.datasource.question.remote +import com.peto.droidmorning.data.model.LikeRequest import com.peto.droidmorning.data.model.QuestionResponse +import io.github.jan.supabase.auth.Auth import io.github.jan.supabase.postgrest.Postgrest -import io.github.jan.supabase.postgrest.query.Columns -import io.github.jan.supabase.postgrest.query.Order -import io.github.jan.supabase.postgrest.query.filter.TextSearchType +import io.github.jan.supabase.postgrest.rpc class DefaultRemoteQuestionDataSource( private val postgrest: Postgrest, + private val auth: Auth, ) : RemoteQuestionDataSource { - override suspend fun fetchQuestions(): List = - postgrest - .from(TABLE_NAME) - .select(columns = Columns.ALL) { - order(column = ORDER_BY_CREATED_AT, order = Order.DESCENDING) - }.decodeList() + private fun uid(): String = + auth.currentSessionOrNull()?.user?.id + ?: throw IllegalStateException("User not logged in") + + override suspend fun fetchQuestions(): List { + val params = mapOf(RPC_FETCH_QUESTIONS_PARAM_NAME to uid()) + return postgrest + .rpc(RPC_FETCH_QUESTIONS, params) + .decodeList() + } - override suspend fun fetchQuestionsByCategory(category: String): List = + override suspend fun addLike(questionId: Long) { + val request = LikeRequest(uid(), questionId) postgrest - .from(TABLE_NAME) - .select(columns = Columns.ALL) { - filter { - eq(CATEGORY_COLUMN, category) - } - order(column = ORDER_BY_CREATED_AT, order = Order.DESCENDING) - }.decodeList() + .from(FAVORITES_TABLE) + .insert(request) + } - override suspend fun searchQuestions(query: String): List = + override suspend fun removeLike(questionId: Long) { postgrest - .from(TABLE_NAME) - .select(columns = Columns.ALL) { + .from(FAVORITES_TABLE) + .delete { filter { - textSearch(FTS_COLUMN, query, TextSearchType.WEBSEARCH) + eq(USER_ID_COLUMN, uid()) + eq(QUESTION_ID_COLUMN, questionId) } - order(column = ORDER_BY_CREATED_AT, order = Order.DESCENDING) - }.decodeList() + } + } companion object { - private const val TABLE_NAME = "questions" - private const val CATEGORY_COLUMN = "category" - private const val FTS_COLUMN = "fts" - private const val ORDER_BY_CREATED_AT = "created_at" + private const val RPC_FETCH_QUESTIONS = "fetch_questions" + private const val RPC_FETCH_QUESTIONS_PARAM_NAME = "uid" + + private const val FAVORITES_TABLE = "favorites" + private const val USER_ID_COLUMN = "user_id" + private const val QUESTION_ID_COLUMN = "question_id" } } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt index 6a6ed26..7e52b8a 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt @@ -5,7 +5,7 @@ import com.peto.droidmorning.data.model.QuestionResponse interface RemoteQuestionDataSource { suspend fun fetchQuestions(): List - suspend fun fetchQuestionsByCategory(category: String): List + suspend fun addLike(questionId: Long) - suspend fun searchQuestions(query: String): List + suspend fun removeLike(questionId: Long) } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt index cd10026..ed975de 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt @@ -12,5 +12,5 @@ internal val dataSourceModule = module { single { DefaultLocalAuthDataSource(get()) } single { DefaultRemoteAuthDataSource(get()) } - single { DefaultRemoteQuestionDataSource(get()) } + single { DefaultRemoteQuestionDataSource(get(), get()) } } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt new file mode 100644 index 0000000..6104014 --- /dev/null +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt @@ -0,0 +1,12 @@ +package com.peto.droidmorning.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LikeRequest( + @SerialName("user_id") + val userId: String, + @SerialName("question_id") + val questionId: Long, +) diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt index 33a7a6f..d8f7eaa 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt @@ -17,6 +17,10 @@ data class QuestionResponse( val createdAt: Instant, @SerialName("updated_at") val updatedAt: Instant, + @SerialName("is_favorited") + val isLiked: Boolean = false, + @SerialName("is_solved") + val isSolved: Boolean = false, ) { fun toDomain(): Question = Question( @@ -26,5 +30,7 @@ data class QuestionResponse( sourceUrl = sourceUrl, createdAt = createdAt, updatedAt = updatedAt, + isSolved = isSolved, + isLiked = isLiked, ) } diff --git a/data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt b/data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt index 9ba7808..e453901 100644 --- a/data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt +++ b/data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt @@ -1,7 +1,6 @@ package com.peto.droidmorning.data.repository import com.peto.droidmorning.data.datasource.question.remote.RemoteQuestionDataSource -import com.peto.droidmorning.domain.model.Category import com.peto.droidmorning.domain.model.Questions import com.peto.droidmorning.domain.repository.QuestionRepository @@ -17,21 +16,16 @@ class DefaultQuestionRepository( Questions(result) } - override suspend fun fetchQuestionsByCategory(category: Category): Result = + override suspend fun toggleQuestionLike( + questionId: Long, + isCurrentlyLiked: Boolean, + ): Result = runCatching { - val result = - remoteQuestionDataSource - .fetchQuestionsByCategory(category.name) - .map { response -> response.toDomain() } - Questions(result) - } - - override suspend fun searchQuestions(query: String): Result = - runCatching { - val result = - remoteQuestionDataSource - .searchQuestions(query) - .map { response -> response.toDomain() } - Questions(result) + if (isCurrentlyLiked) { + remoteQuestionDataSource.removeLike(questionId) + } else { + remoteQuestionDataSource.addLike(questionId) + } + true } } diff --git a/data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt b/data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt index 24cab77..33df236 100644 --- a/data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt +++ b/data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt @@ -6,10 +6,21 @@ import com.peto.droidmorning.data.model.QuestionResponse class FakeRemoteQuestionDataSource( private val questions: List, ) : RemoteQuestionDataSource { + private val likedQuestions = mutableSetOf() + override suspend fun fetchQuestions(): List = questions - override suspend fun fetchQuestionsByCategory(category: String): List = questions.filter { it.category == category } + override suspend fun addLike(questionId: Long) { + likedQuestions.add(questionId) + } + + override suspend fun removeLike(questionId: Long) { + likedQuestions.remove(questionId) + } + + fun isLiked(questionId: Long): Boolean = likedQuestions.contains(questionId) - override suspend fun searchQuestions(query: String): List = - questions.filter { it.title.contains(query, ignoreCase = true) } + fun clearLikes() { + likedQuestions.clear() + } } diff --git a/data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt b/data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt index 5c507b7..89247d8 100644 --- a/data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt +++ b/data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt @@ -2,7 +2,6 @@ package com.peto.droidmorning.data.repository import com.peto.droidmorning.data.fake.FakeRemoteQuestionDataSource import com.peto.droidmorning.data.fixture.QuestionResponseFixture -import com.peto.droidmorning.domain.model.Category import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -28,139 +27,41 @@ class DefaultQuestionRepositoryTest { } @Test - fun `fetchQuestionsByCategory Kotlin은 Kotlin 카테고리만 반환한다`() = + fun `toggleQuestionLike는 좋아요가 추가되면 true를 반환한다`() = runTest { // given - val responses = - listOf( - QuestionResponseFixture.questionResponse( - id = 1L, - title = "match-${Category.Kotlin.name}", - category = Category.Kotlin, - ), - QuestionResponseFixture.questionResponse( - id = 2L, - title = "other-${Category.Android.name}", - category = Category.Android, - ), - ) - val fakeDataSource = FakeRemoteQuestionDataSource(questions = responses) - val repository = DefaultQuestionRepository(fakeDataSource) - - // when - val result = repository.fetchQuestionsByCategory(Category.Kotlin) - - // then - val questions = result.getOrThrow() - assertEquals(1, questions.size) - assertEquals(Category.Kotlin, questions.toList().first().category) - } - - @Test - fun `fetchQuestionsByCategory Coroutine은 Coroutine 카테고리만 반환한다`() = - runTest { - // given - val responses = - listOf( - QuestionResponseFixture.questionResponse( - id = 1L, - title = "match-${Category.Coroutine.name}", - category = Category.Coroutine, - ), - QuestionResponseFixture.questionResponse( - id = 2L, - title = "other-${Category.Compose.name}", - category = Category.Compose, - ), - ) - val fakeDataSource = FakeRemoteQuestionDataSource(questions = responses) - val repository = DefaultQuestionRepository(fakeDataSource) - - // when - val result = repository.fetchQuestionsByCategory(Category.Coroutine) - - // then - val questions = result.getOrThrow() - assertEquals(1, questions.size) - assertEquals(Category.Coroutine, questions.toList().first().category) - } - - @Test - fun `fetchQuestionsByCategory Android는 Android 카테고리만 반환한다`() = - runTest { - // given - val responses = - listOf( - QuestionResponseFixture.questionResponse( - id = 1L, - title = "match-${Category.Android.name}", - category = Category.Android, - ), - QuestionResponseFixture.questionResponse( - id = 2L, - title = "other-${Category.Kotlin.name}", - category = Category.Kotlin, - ), - ) + val responses = QuestionResponseFixture.questionResponseList() val fakeDataSource = FakeRemoteQuestionDataSource(questions = responses) val repository = DefaultQuestionRepository(fakeDataSource) + val questionId = 1L // when - val result = repository.fetchQuestionsByCategory(Category.Android) + val result = repository.toggleQuestionLike(questionId, isCurrentlyLiked = false) // then - val questions = result.getOrThrow() - assertEquals(1, questions.size) - assertEquals(Category.Android, questions.toList().first().category) + assertTrue(result.isSuccess) + assertTrue(result.getOrThrow()) + assertTrue(fakeDataSource.isLiked(questionId)) } @Test - fun `fetchQuestionsByCategory Compose는 Compose 카테고리만 반환한다`() = + fun `toggleQuestionLike는 좋아요가 제거되면 true를 반환한다`() = runTest { // given - val responses = - listOf( - QuestionResponseFixture.questionResponse( - id = 1L, - title = "match-${Category.Compose.name}", - category = Category.Compose, - ), - QuestionResponseFixture.questionResponse( - id = 2L, - title = "other-${Category.Coroutine.name}", - category = Category.Coroutine, - ), - ) + val responses = QuestionResponseFixture.questionResponseList() val fakeDataSource = FakeRemoteQuestionDataSource(questions = responses) val repository = DefaultQuestionRepository(fakeDataSource) + val questionId = 1L - // when - val result = repository.fetchQuestionsByCategory(Category.Compose) - - // then - val questions = result.getOrThrow() - assertEquals(1, questions.size) - assertEquals(Category.Compose, questions.toList().first().category) - } - - @Test - fun `searchQuestions는 query가 포함된 질문만 반환한다`() = - runTest { - // given - val responses = - listOf( - QuestionResponseFixture.questionResponse(title = "Kotlin Coroutines"), - QuestionResponseFixture.questionResponse(title = "Swift Concurrency"), - ) - val fakeDataSource = FakeRemoteQuestionDataSource(questions = responses) - val repository = DefaultQuestionRepository(fakeDataSource) + // 먼저 좋아요 추가 + fakeDataSource.addLike(questionId) // when - val result = repository.searchQuestions("Kotlin") + val result = repository.toggleQuestionLike(questionId, isCurrentlyLiked = true) // then - val questions = result.getOrThrow() - assertEquals(1, questions.size) - assertEquals("Kotlin Coroutines", questions.toList().first().title) + assertTrue(result.isSuccess) + assertTrue(result.getOrThrow()) + assertTrue(!fakeDataSource.isLiked(questionId)) } } diff --git a/designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt b/designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt index b5a6b76..f098941 100644 --- a/designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt +++ b/designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt @@ -46,9 +46,9 @@ fun QuestionCard( title: String, category: Category, isSolved: Boolean, - isFavorite: Boolean, + isLiked: Boolean, onClick: () -> Unit, - onFavoriteClick: () -> Unit, + onLikeClick: () -> Unit, modifier: Modifier = Modifier, ) { InteractiveCard( @@ -92,19 +92,19 @@ fun QuestionCard( } IconButton( - onClick = onFavoriteClick, + onClick = onLikeClick, content = { Icon( - imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, + imageVector = if (isLiked) Icons.Filled.Star else Icons.Outlined.Star, contentDescription = stringResource( - if (isFavorite) { + if (isLiked) { DesignRes.string.question_card_favorite_remove } else { DesignRes.string.question_card_favorite_add }, ), - tint = if (isFavorite) Warning else MutedForeground, + tint = if (isLiked) Warning else MutedForeground, modifier = Modifier.size(Dimen.iconSm), ) }, @@ -134,9 +134,9 @@ private fun QuestionCardPreview( title = state.title, category = state.category, isSolved = state.isSolved, - isFavorite = state.isFavorite, + isLiked = state.isFavorite, onClick = {}, - onFavoriteClick = {}, + onLikeClick = {}, ) } } diff --git a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt index e3c20fd..fe8e5cf 100644 --- a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt +++ b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt @@ -22,7 +22,7 @@ data class Filter( fun clearSolvedFilter(): Filter = copy(solved = false) - fun applyFavoritesFilter(): Filter = copy(liked = true) + fun applyLikedFilter(): Filter = copy(liked = true) - fun clearFavoritesFilter(): Filter = copy(liked = false) + fun clearLikedFilter(): Filter = copy(liked = false) } diff --git a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt index 08832b9..14ef2e2 100644 --- a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt +++ b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt @@ -9,8 +9,8 @@ data class Question( val sourceUrl: String, val createdAt: Instant, val updatedAt: Instant, - val isSolved: Boolean = false, - val isFavorite: Boolean = false, + val isSolved: Boolean, + val isLiked: Boolean, ) { fun isTitleMatched(query: SearchQuery): Boolean = title.contains(query.value, ignoreCase = true) } diff --git a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt index 906f576..34a3bfa 100644 --- a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt +++ b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt @@ -13,7 +13,7 @@ data class Questions( filterBySearchQuery(filter.searchQuery) .filterByCategory(filter.categories) .filterBySolved(filter.solved) - .filterByFavorite(filter.liked) + .filterByLiked(filter.liked) private fun filterBySearchQuery(query: SearchQuery): Questions { if (query.isEmpty()) return this @@ -40,9 +40,9 @@ data class Questions( return copy(values = values.filter { it.isSolved }) } - private fun filterByFavorite(showFavoritesOnly: Boolean): Questions { - if (!showFavoritesOnly) return this - return copy(values = values.filter { it.isFavorite }) + private fun filterByLiked(showLikedOnly: Boolean): Questions { + if (!showLikedOnly) return this + return copy(values = values.filter { it.isLiked }) } fun toList(): List = values.toList() diff --git a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt index 961eaa8..d8760ab 100644 --- a/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt +++ b/domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt @@ -1,12 +1,12 @@ package com.peto.droidmorning.domain.repository -import com.peto.droidmorning.domain.model.Category import com.peto.droidmorning.domain.model.Questions interface QuestionRepository { suspend fun fetchQuestions(): Result - suspend fun fetchQuestionsByCategory(category: Category): Result - - suspend fun searchQuestions(query: String): Result + suspend fun toggleQuestionLike( + questionId: Long, + isCurrentlyLiked: Boolean, + ): Result } diff --git a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt index 0a504f8..bd3abd6 100644 --- a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt +++ b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt @@ -151,7 +151,7 @@ class FilterTest { // Given // When - val result = emptyFilter.applyFavoritesFilter() + val result = emptyFilter.applyLikedFilter() // Then assertTrue(result.liked) @@ -162,7 +162,7 @@ class FilterTest { // Given // When - val result = filterWithFavorites.clearFavoritesFilter() + val result = filterWithFavorites.clearLikedFilter() // Then assertFalse(result.liked) diff --git a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt index 5d34fda..3b9026f 100644 --- a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt +++ b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt @@ -20,7 +20,7 @@ class QuestionTest { category: Category = Category.Kotlin, sourceUrl: String = "https://example.com", isSolved: Boolean = false, - isFavorite: Boolean = false, + isLiked: Boolean = false, ) = Question( id = id, title = title, @@ -29,7 +29,7 @@ class QuestionTest { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = isSolved, - isFavorite = isFavorite, + isLiked = isLiked, ) @Test diff --git a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt index 5a9d5a8..c4f8487 100644 --- a/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt +++ b/domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt @@ -49,9 +49,9 @@ class QuestionsTest { mixedFavoriteQuestions = Questions( listOf( - createQuestion(id = 1, isFavorite = true), - createQuestion(id = 2, isFavorite = false), - createQuestion(id = 3, isFavorite = true), + createQuestion(id = 1, isLiked = true), + createQuestion(id = 2, isLiked = false), + createQuestion(id = 3, isLiked = true), ), ) } @@ -61,7 +61,7 @@ class QuestionsTest { title: String = "Test Question", category: Category = Category.Kotlin, isSolved: Boolean = false, - isFavorite: Boolean = false, + isLiked: Boolean = false, ) = Question( id = id, title = title, @@ -70,7 +70,7 @@ class QuestionsTest { createdAt = Instant.fromEpochMilliseconds(0), updatedAt = Instant.fromEpochMilliseconds(0), isSolved = isSolved, - isFavorite = isFavorite, + isLiked = isLiked, ) @Test @@ -182,7 +182,7 @@ class QuestionsTest { // Then assertAll( { assertEquals(2, result.size) }, - { assertTrue(result.toList().all { it.isFavorite }) }, + { assertTrue(result.toList().all { it.isLiked }) }, ) }