Skip to content

Comments

질문 좋아요 기능 구현#32

Merged
chanho0908 merged 8 commits intomainfrom
31-질문-좋아요-푼-문제-확인-기능-구현
Jan 15, 2026

Hidden character warning

The head ref may contain hidden characters: "31-\uc9c8\ubb38-\uc88b\uc544\uc694-\ud47c-\ubb38\uc81c-\ud655\uc778-\uae30\ub2a5-\uad6c\ud604"
Merged

질문 좋아요 기능 구현#32
chanho0908 merged 8 commits intomainfrom
31-질문-좋아요-푼-문제-확인-기능-구현

Conversation

@chanho0908
Copy link
Owner

@chanho0908 chanho0908 commented Jan 15, 2026

이슈 번호

Closes #31

작업내용

질문에 좋아요를 표시하고 관리할 수 있는 기능을 구현했습니다.

주요 변경사항

Domain Layer:

  • QuestionRepositorytoggleQuestionLike() 메서드 추가
  • Question 모델에 isLiked, isSolved 필드 추가
  • Filter 모델의 메서드명 변경: applyFavoritesFilterapplyLikedFilter
  • fetchQuestionsByCategory, searchQuestions 제거 (클라이언트 필터링으로 대체)

Data Layer:

  • RemoteQuestionDataSourceaddLike, removeLike 메서드 추가
  • Supabase favorites 테이블에 직접 insert/delete 방식으로 구현
  • LikeRequest 모델 추가하여 좋아요 요청 데이터 직렬화
  • Supabase RPC 함수 fetch_questions 연동하여 사용자별 좋아요 상태 조회

Presentation Layer:

  • QuestionViewModelonLikeToggle() 메서드 구현
  • 낙관적 업데이트(Optimistic Update) 적용:
    • UI를 먼저 업데이트하여 즉각적인 반응 제공
    • 서버 요청 실패 시 이전 상태로 자동 롤백
    • 성공 시 질문 목록 새로고침하여 정확한 상태 동기화
  • QuestionUiStatetoggleQuestionLike() 메서드 추가

Test:

  • FakeRemoteQuestionDataSource에 좋아요 상태 추적 기능 추가
  • toggleQuestionLike 테스트 케이스 추가 (추가/제거)

결과물

dss.mp4

chanho0908 and others added 7 commits January 15, 2026 17:02
- Question 도메인 모델에 isSolved, isFavorite 필수 필드로 추가
- QuestionResponse 데이터 모델에 is_favorited, is_solved 필드 추가
- toDomain() 매핑 로직에 새 필드 반영

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
- Supabase RPC 함수(fetch_questions) 사용으로 변경
- Auth 모듈 의존성 추가하여 사용자 ID 기반 조회
- 좋아요/푼 문제 상태가 포함된 질문 목록 조회 가능
- DataSourceModule에 Auth 의존성 주입 추가

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
- QuestionRepository에 toggleQuestionLike 메서드 추가
- RemoteQuestionDataSource에 addLike, removeLike 메서드 추가
- Supabase favorites 테이블 직접 insert/delete로 구현
- LikeRequest 모델 추가하여 좋아요 요청 직렬화
- fetchQuestionsByCategory, searchQuestions 메서드 제거 (클라이언트 필터링으로 대체)

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
- FakeRemoteQuestionDataSource에 좋아��� 상태 추적 기능 추가
- toggleQuestionLike 테스트 케이스 2개 추가 (추가/제거)
- fetchQuestionsByCategory, searchQuestions 테스트 제거

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
- QuestionViewModel에 onLikeToggle 메서드 구현
- 낙관적 업데이트(Optimistic Update) 적용하여 즉각적인 UI 반응
- 서버 요청 실패 시 이전 상태로 롤백
- QuestionUiState에 toggleQuestionLike 메서드 추가하여 특정 질문의 좋아요 상태 토글

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
- FilterTest: applyFavoritesFilter → applyLikedFilter
- FilterTest: clearFavoritesFilter → clearLikedFilter
- QuestionTest, QuestionsTest: isFavorite → isLiked

🤖 Generated with [Firebender](https://firebender.com)

Co-Authored-By: Firebender <help@firebender.com>
@chanho0908 chanho0908 linked an issue Jan 15, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

이 PR은 전체 코드베이스에서 "favorites" 용어를 "liked"로 리브랜딩하고 질문 좋아요(Like) 기능을 구현하기 위한 변경입니다. UI 컴포넌트(QuestionScreen, QuestionFilterChips, QuestionList, QuestionCard)와 ViewModel의 파라미터/메서드명이 변경되었고, ViewModel에 onLikeToggle(낙관적 업데이트 포함)이 추가되었습니다. 도메인 모델은 isFavoriteisLiked로 변경되고 Filter 관련 메서드명이 업데이트되었습니다. 리포지토리 및 원격 데이터소스 인터페이스가 카테고리/검색 메서드 제거 후 addLike/removeLike와 toggleQuestionLike로 재구성되었고, DefaultRemoteQuestionDataSource에 Auth 주입과 RPC 호출로의 전환이 포함됩니다. 테스트 코드와 페이크 데이터소스도 이에 맞춰 갱신되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive 변경사항이 대부분 좋아요 기능 구현에 집중되어 있으나, fetchQuestionsByCategory와 searchQuestions 메서드 제거는 주요 요구사항이 아닌 리팩토링입니다. fetchQuestionsByCategory와 searchQuestions 제거가 좋아요 기능 구현과의 관계를 명확히 하세요. 클라이언트 필터링으로 대체된 것이 의도적인 것인지, 다른 이슈에서 수행할 작업인지 확인이 필요합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경 내용을 명확하게 반영하고 있습니다. '질문 좋아요 기능 구현'은 전체 변경사항의 핵심 목표를 직관적으로 전달합니다.
Description check ✅ Passed PR 설명이 변경사항을 매우 상세하게 기술하고 있습니다. Domain/Data/Presentation/Test 계층별 주요 변경사항과 낙관적 업데이트 패턴 등을 구체적으로 설명하고 있습니다.
Linked Issues check ✅ Passed PR이 링크된 이슈 #31의 모든 주요 요구사항을 충족합니다. 질문 좋아요 추가/삭제 기능이 Domain/Data/Presentation 계층 전반에서 완전히 구현되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9da08 and 21d8870.

📒 Files selected for processing (1)
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
🧰 Additional context used
📓 Path-based instructions (1)
data/**

⚙️ CodeRabbit configuration file

data/**: [Data 모듈 리뷰 가이드]

  1. Repository 구현
  • Domain 인터페이스를 정확히 구현하는가?
  • Remote / Local 데이터 소스 조합 로직이 명확한가?
  • 에러가 도메인 레벨로 적절히 변환되는가?
  1. Mapper
  • DTO → Domain 변환 책임이 명확한가?
  • null safety가 충분히 고려되었는가?

Files:

  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
🧠 Learnings (1)
📚 Learning: 2026-01-05T11:57:41.443Z
Learnt from: chanho0908
Repo: chanho0908/DroidMorning PR: 16
File: data/src/commonTest/kotlin/com/peto/droidmorning/data/fixture/QuestionResponseFixture.kt:5-5
Timestamp: 2026-01-05T11:57:41.443Z
Learning: In Kotlin (Multiplatform) projects, prefer kotlin.time.Instant over kotlinx.datetime.Instant because kotlinx.datetime.Instant is deprecated since its 0.7.x transition. Apply in all Kotlin source files where Instant is used, including test fixtures like data/fixture files. Replace imports from kotlinx.datetime.Instant to kotlin.time.Instant and adjust any API usage accordingly.

Applied to files:

  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Android CI / android-build
🔇 Additional comments (4)
data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt (4)

13-15: 이전 리뷰에서 제안한 uid() 메서드 방식이 잘 적용되었습니다! 👍

생성자 시점에 uidval로 캐싱하던 방식에서, 매 호출 시점에 현재 세션의 사용자 ID를 가져오는 메서드 방식으로 변경하셨네요.

이 변경으로 인해:

  • 싱글톤 스코프에서 사용자 재인증 시 발생할 수 있는 데이터 무결성 문제가 해결됩니다
  • DI 컨테이너 초기화 시점의 로그인 상태 의존성이 제거됩니다

17-22: RPC 기반 질문 조회 구현이 깔끔합니다.

Supabase RPC 호출을 통해 서버 사이드에서 사용자별 좋아요 상태를 포함한 질문 목록을 조회하는 방식이 잘 구현되었습니다. uid()를 호출 시점에 가져오므로 항상 현재 인증된 사용자의 데이터를 조회합니다.


42-49: 상수 정의가 명확합니다.

RPC 함수명, 파라미터명, 테이블명, 컬럼명을 companion object 내에 상수로 정의하여 매직 스트링 사용을 방지하고 유지보수성을 높였습니다.


24-40: 좋아요 추가/제제거 구현이 적절합니다.

addLikeremoveLike 구현이 명확하고, 각각 insert/delete 방식으로 favorites 테이블을 관리합니다. uid()를 호출 시점에 가져오므로 현재 로그인한 사용자의 좋아요 상태를 정확히 관리합니다.

특히 이미 좋아요가 존재할 때 발생할 수 있는 데이터베이스 제약 조건 위반(unique constraint violation)에 대해 좋은 처리가 이루어져 있습니다. ViewModel의 onLikeToggle 메서드에서:

  1. 낙관적 업데이트: UI를 즉시 토글하여 사용자에게 빠른 반응성을 제공
  2. 에러 처리: Repository 호출의 .onFailure 블록에서 예외를 캐치
  3. 롤백: 실패 시 UI 상태를 원래대로 되돌림
  4. 사용자 알림: ShowError 이벤트를 통해 실패 사실을 전달

이러한 패턴은 네트워크 불안정성이나 중복 제약 조건 위반 등의 문제를 우아하게 처리하는 업계 표준입니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chanho0908 chanho0908 self-assigned this Jan 15, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt`:
- Around line 85-101: The onLikeToggle function currently looks up the question
in _uiState.value.filteredQuestions which causes toggles to be ignored for items
hidden by filters; change the lookup to search the full list (e.g.,
_uiState.value.allQuestions.find { it.id == questionId } or expose an
allQuestions accessor) so the optimistic update and rollback (uses
toggleQuestionLike) always operate on the canonical item, leaving the rest of
the flow (calling _uiState.update { it.toggleQuestionLike(questionId) },
repository.toggleQuestionLike(...) and the onFailure rollback/sendUiEvent)
unchanged.

In
`@data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt`:
- Around line 9-15: DefaultRemoteQuestionDataSource currently caches uid as a
constructor val which breaks correctness for the singleton scope; instead remove
the uid field and fetch the current uid on each API call by calling
auth.currentSessionOrNull()?.user?.id inside the methods that need it (e.g.,
like/unlike or other user-scoped operations in DefaultRemoteQuestionDataSource),
and throw IllegalStateException at call-time if null so the class no longer
depends on login state at construction; keep auth as a constructor property to
access session info and update all usages that referenced the old uid to use the
locally fetched uid variable.
🧹 Nitpick comments (6)
domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt (1)

15-23: 선택적 개선 제안: 변수명 일관성 고려

현재 filterWithFavorites 변수명이 "Favorites" 용어를 사용하고 있는데, PR 전반에서 "liked" 용어로 통일하고 있으므로 filterWithLiked로 변경하면 일관성이 더 높아질 수 있습니다. 기능에는 영향이 없으므로 선택적으로 고려해 주세요.

♻️ 제안된 변경
-    private lateinit var filterWithFavorites: Filter
+    private lateinit var filterWithLiked: Filter

     `@BeforeTest`
     fun setup() {
         emptyFilter = Filter()
         filterWithQuery = Filter(searchQuery = SearchQuery("kotlin"))
         filterWithCategory = Filter(categories = Categories(setOf(Category.Kotlin)))
         filterWithSolved = Filter(solved = true)
-        filterWithFavorites = Filter(liked = true)
+        filterWithLiked = Filter(liked = true)
     }
data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt (1)

15-15: Koin 의존성 주입이 올바르게 구성되었습니다.

DefaultRemoteQuestionDataSource가 이제 PostgrestAuth 두 개의 의존성을 받도록 업데이트되었네요. 기능적으로 올바르게 동작합니다.

다만, 명시적 타입 지정을 고려해 보시면 가독성과 유지보수성이 향상될 수 있습니다:

♻️ 선택적 개선 제안
-        single<RemoteQuestionDataSource> { DefaultRemoteQuestionDataSource(get(), get()) }
+        single<RemoteQuestionDataSource> { DefaultRemoteQuestionDataSource(get<Postgrest>(), get<Auth>()) }
domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt (1)

16-16: 변수명 일관성 개선을 고려해 주세요.

mixedFavoriteQuestions라는 변수명이 이제 isLiked 필드를 테스트하는 데 사용되고 있어 용어가 혼재되어 있습니다. 코드베이스 전반에서 favoriteliked로 용어를 통일하는 PR 취지에 맞게 변수명도 변경하면 좋을 것 같습니다.

♻️ 제안된 변경
-    private lateinit var mixedFavoriteQuestions: Questions
+    private lateinit var mixedLikedQuestions: Questions

그리고 Line 49와 Line 180에서 해당 변수 사용부도 함께 수정해 주세요.

Also applies to: 49-56

designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt (1)

126-142: QuestionCardPreviewState의 프로퍼티명을 isLiked로 통일하는 것을 권장합니다.

현재 QuestionCard 컴포넌트는 isLiked 파라미터를 사용하지만, 프리뷰용 상태 객체인 QuestionCardPreviewStateisFavorite 프로퍼티를 사용하고 있어 명명 규칙이 일치하지 않습니다. 이로 인해 라인 137에서 isLiked = state.isFavorite처럼 매핑이 필요한 상황이 발생합니다.

개선 방안:
QuestionCardPreviewProvider.kt에서 isFavoriteisLiked로 변경하면 다음과 같은 이점이 있습니다:

  • 컴포넌트 API와 프리뷰 상태의 용어가 통일되어 의도가 명확해집니다
  • 앞으로 유사한 프리뷰를 작성할 때 일관된 패턴을 제공합니다
  • 다른 파일에서만 사용되므로 변경 범위가 최소화됩니다

이는 기능상 문제는 없지만, 코드베이스 일관성 관점에서 개선할 가치가 있는 부분입니다.

data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt (2)

63-65: assertFalse 사용을 권장합니다.

assertTrue(!condition) 대신 assertFalse(condition)를 사용하면 가독성이 향상되고, 테스트 실패 시 더 명확한 에러 메시지를 제공받을 수 있습니다. 예를 들어 assertTrue(!x)가 실패하면 단순히 "Expected true, got false"라는 메시지가 나오지만, assertFalse(x)는 의도를 더 명확하게 전달합니다.

♻️ 제안하는 수정사항

import 추가:

import kotlin.test.assertFalse

assertion 변경:

-            assertTrue(!fakeDataSource.isLiked(questionId))
+            assertFalse(fakeDataSource.isLiked(questionId))

10-66: 테스트 커버리지 확장을 고려해 보세요.

현재 성공 케이스에 대한 테스트가 잘 작성되어 있습니다. 추후 안정성을 높이기 위해 다음과 같은 실패 시나리오 테스트를 추가하면 더 견고한 테스트 스위트가 될 것입니다:

  • 네트워크 오류 시 Result.failure 반환 검증
  • 존재하지 않는 questionId에 대한 처리

이 부분은 현재 PR 범위 외로 미뤄도 무방하며, 별도 이슈로 추적하셔도 좋을 것 같습니다.

실패 시나리오 테스트 코드 생성이나 이를 추적할 이슈 생성을 도와드릴까요?

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b937c04 and 6e9da08.

📒 Files selected for processing (21)
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt
  • designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt
🧰 Additional context used
📓 Path-based instructions (4)
data/**

⚙️ CodeRabbit configuration file

data/**: [Data 모듈 리뷰 가이드]

  1. Repository 구현
  • Domain 인터페이스를 정확히 구현하는가?
  • Remote / Local 데이터 소스 조합 로직이 명확한가?
  • 에러가 도메인 레벨로 적절히 변환되는가?
  1. Mapper
  • DTO → Domain 변환 책임이 명확한가?
  • null safety가 충분히 고려되었는가?

Files:

  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt
domain/**

⚙️ CodeRabbit configuration file

domain/**: [Domain 모듈 리뷰 가이드]

  1. 아키텍처
  • Android 및 외부 프레임워크 의존성이 없는가?
  • 비즈니스 로직이 명확히 표현되어 있는가?
  • 도메인 모델이 불변 객체로 설계되었는가?

Files:

  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt
composeApp/src/commonMain/kotlin/**/*.kt

📄 CodeRabbit inference engine (README.md)

Common Kotlin code that targets all platforms should be placed in commonMain

Files:

  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt
composeApp/src/**/*.kt

📄 CodeRabbit inference engine (README.md)

Enforce consistent Kotlin code style using ktlint

Files:

  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt
🧠 Learnings (1)
📚 Learning: 2026-01-05T11:57:41.443Z
Learnt from: chanho0908
Repo: chanho0908/DroidMorning PR: 16
File: data/src/commonTest/kotlin/com/peto/droidmorning/data/fixture/QuestionResponseFixture.kt:5-5
Timestamp: 2026-01-05T11:57:41.443Z
Learning: In Kotlin (Multiplatform) projects, prefer kotlin.time.Instant over kotlinx.datetime.Instant because kotlinx.datetime.Instant is deprecated since its 0.7.x transition. Apply in all Kotlin source files where Instant is used, including test fixtures like data/fixture files. Replace imports from kotlinx.datetime.Instant to kotlin.time.Instant and adjust any API usage accordingly.

Applied to files:

  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/di/DataSourceModule.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt
  • designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt
  • domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt
  • data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt
  • composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt
  • data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt
  • domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt
🧬 Code graph analysis (2)
composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt (1)
composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt (1)
  • onLikeToggle (85-101)
domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt (1)
domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt (1)
  • createQuestion (17-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Android CI / android-build
🔇 Additional comments (32)
domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Filter.kt (1)

25-27: LGTM! 메서드 명칭 변경이 적절합니다.

applyFavoritesFilter()applyLikedFilter(), clearFavoritesFilter()clearLikedFilter()로 명칭을 변경하여 PR 전반의 "liked" 용어 통일과 일관성을 유지하고 있습니다. 불변 객체 패턴(copy())을 유지하며, 외부 프레임워크 의존성 없이 순수한 도메인 로직을 표현하고 있어 도메인 모듈 가이드라인을 잘 준수했습니다.

domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionTest.kt (1)

17-33: LGTM! 테스트 헬퍼 함수가 도메인 모델 변경에 맞게 잘 업데이트되었습니다.

createQuestion() 헬퍼 함수에서 isFavoriteisLiked로 파라미터 및 필드 명을 변경하여 도메인 모델(Question)의 변경 사항과 일관성을 유지하고 있습니다. 또한 kotlin.time.Instant를 사용하고 있어 deprecated된 kotlinx.datetime.Instant 대신 올바른 API를 사용하고 있습니다.

domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/FilterTest.kt (1)

154-165: LGTM! 메서드 호출이 올바르게 업데이트되었습니다.

applyLikedFilter()clearLikedFilter() 호출로 변경하여 Filter.kt의 API 변경 사항과 일치합니다. 테스트 로직과 검증이 정확하게 유지되고 있습니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionUiState.kt (3)

26-26: LGTM! 프로퍼티 명칭 변경이 적절합니다.

showFavoritesOnlyshowLikedOnly로 변경하여 PR 전반의 "liked" 용어 통일과 일관성을 유지하고 있습니다.


51-53: LGTM! 필터 메서드 명칭이 도메인 모델과 일관되게 변경되었습니다.

applyLikedFilter()clearLikedFilter()로 변경하여 Filter.kt 도메인 모델의 API와 일치합니다.


55-65: LGTM! 낙관적 업데이트를 위한 toggleQuestionLike 구현이 잘 되어 있습니다.

불변성을 유지하면서 mapcopy()를 사용하여 특정 질문의 isLiked 상태를 토글하는 로직이 명확합니다. 이 함수는 ViewModel의 낙관적 업데이트 패턴을 지원하여 UI 응답성을 높이는 데 기여합니다.

한 가지 고려할 점: questionId가 존재하지 않는 경우에도 예외 없이 동일한 상태를 반환하므로 안전하게 동작합니다. 만약 존재하지 않는 ID에 대한 처리가 필요하다면 로깅이나 별도 처리를 추가할 수 있지만, 현재 낙관적 업데이트 시나리오에서는 항상 유효한 ID가 전달될 것으로 예상되므로 현재 구현이 적절합니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/model/QuestionResponse.kt (3)

20-24: LGTM! 새로운 필드 추가가 적절합니다.

isLikedisSolved 필드가 기본값 false와 함께 추가되어 null safety가 보장됩니다. @SerialName("is_favorited")으로 백엔드의 "favorited" 용어를 앱의 "liked" 용어로 매핑하는 것은 Data 레이어의 DTO 변환 책임에 부합합니다.

참고로, 백엔드 스키마가 향후 "is_liked"로 변경된다면 @SerialName만 수정하면 되므로 유지보수성도 좋습니다.


25-35: LGTM! toDomain() 매퍼가 올바르게 업데이트되었습니다.

isSolvedisLiked 필드가 도메인 모델 Question에 정확하게 매핑되고 있습니다. Data 모듈 가이드라인에 따라 DTO → Domain 변환 책임이 명확하게 구현되어 있습니다.


7-7: 좋습니다! kotlin.time.Instant 사용이 올바릅니다.

deprecated된 kotlinx.datetime.Instant 대신 kotlin.time.Instant를 사용하고 있어 Kotlin Multiplatform 프로젝트의 권장 사항을 잘 따르고 있습니다. Based on learnings에서 권장하는 대로 올바른 API를 사용하고 있습니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/model/LikeRequest.kt (1)

1-12: 깔끔한 DTO 구현입니다! 👍

LikeRequest 모델이 Data 레이어 가이드라인을 잘 따르고 있습니다:

  • 불변 객체(val 프로퍼티)로 설계됨
  • @SerialName을 통해 snake_case → camelCase 매핑이 명확함
  • Supabase favorites 테이블 스키마와 정확히 일치
domain/src/commonTest/kotlin/com/peto/droidmorning/domain/model/QuestionsTest.kt (2)

52-54: 테스트 코드가 새로운 isLiked API와 잘 맞게 업데이트되었습니다.

createQuestion 헬퍼 함수와 호출부가 일관되게 isLiked 파라미터를 사용하도록 변경되었네요. QuestionTest.kt의 동일한 헬퍼 함수와도 시그니처가 일치합니다.

Also applies to: 64-64, 73-73


185-185: LGTM!

단언문이 isLiked 필드를 올바르게 검증하고 있습니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/QuestionScreen.kt (4)

49-58: 필터 상태 변경 시 스크롤 초기화 로직이 잘 구현되었습니다.

LaunchedEffect의 의존성에 showLikedOnly가 올바르게 추가되어, 좋아요 필터 토글 시에도 리스트가 맨 위로 스크롤됩니다. UX 일관성 측면에서 좋은 패턴입니다.


75-85: ViewModel 콜백 연결이 깔끔합니다.

onLikedFilterToggleonLikeToggle 콜백이 ViewModel의 메서드에 메서드 레퍼런스(::문법)로 연결되어 있어 가독성이 좋습니다.


88-99: 파라미터 명명이 일관되게 업데이트되었습니다.

QuestionScreenContent의 파라미터명이 liked 용어로 통일되어 PR 전반의 리브랜딩 취지에 부합합니다.


115-122: 하위 컴포넌트로의 props 전달이 올바릅니다.

QuestionFilterChipsQuestionList에 새로운 liked 관련 props가 정확하게 전달되고 있습니다. 컴포넌트 계층 간 데이터 흐름이 명확합니다.

Also applies to: 156-161

domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Question.kt (1)

5-14: Domain 모델이 아키텍처 가이드라인을 완벽하게 준수하고 있습니다.

Question 모델의 설계가 Domain 레이어 가이드라인을 잘 따르고 있습니다:

  • 불변 객체(data class)로 설계 ✓
  • Android 및 외부 프레임워크 의존성 없음 ✓
  • kotlin.time.Instant 사용 ✓

API 변경 사항(isSolved, isLiked의 기본값 제거)도 적절하게 구현되었으며, 모든 호출 지점(QuestionResponse.toDomain(), 테스트 헬퍼 함수, Preview 코드 등)이 새로운 시그니처에 맞게 올바르게 업데이트되어 있습니다.

domain/src/commonMain/kotlin/com/peto/droidmorning/domain/repository/QuestionRepository.kt (1)

8-11: 인터페이스 설계가 깔끔합니다!

도메인 레이어에 외부 프레임워크 의존성 없이 순수 Kotlin 인터페이스로 잘 정의되어 있습니다. isCurrentlyLiked 파라미터를 통해 현재 상태를 전달받아 Repository가 추가/삭제를 결정하도록 한 설계가 명확합니다.

한 가지 고려해볼 점: Result<Boolean>에서 Boolean이 항상 true만 반환된다면, Result<Unit>으로 변경하는 것이 의도를 더 명확히 전달할 수 있습니다. 다만 향후 확장성(예: 좋아요 수 반환 등)을 고려한 설계라면 현재 방식도 적절합니다.

domain/src/commonMain/kotlin/com/peto/droidmorning/domain/model/Questions.kt (1)

43-46: 일관된 네이밍과 필터 로직이 잘 구현되었습니다!

filterBySolved와 동일한 패턴으로 filterByLiked가 구현되어 코드 일관성이 유지됩니다. 불변 객체(copy)를 사용한 도메인 모델 설계도 가이드라인에 부합합니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt (1)

62-71: 기존 패턴과 일관된 구현입니다!

onSolvedFilterToggle()과 동일한 패턴으로 구현되어 코드의 일관성이 유지됩니다. applyFilter() 호출을 통해 필터 적용 로직도 정상적으로 연결되어 있습니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepository.kt (1)

19-30: Repository 구현이 깔끔합니다!

도메인 인터페이스를 정확히 구현하고 있으며, runCatching을 통한 에러 핸들링이 적절합니다. isCurrentlyLiked 값에 따른 분기 로직도 명확합니다.

개선 고려사항: 코딩 가이드라인에 따르면 "에러가 도메인 레벨로 적절히 변환되는가"를 검토해야 합니다. 현재는 Data 레이어의 예외가 그대로 Result.failure로 전달됩니다. 필요시 도메인 수준의 커스텀 예외로 변환하는 것을 고려해볼 수 있습니다:

// 예시: 도메인 수준 에러 변환
.onFailure { throw LikeOperationException(it) }

다만, 현재 구현도 동작에는 문제가 없으며, 에러 세분화가 필요할 때 추가하면 됩니다.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionList.kt (2)

21-48: UI 컴포넌트 변경이 잘 적용되었습니다!

onLikeToggle 콜백과 isLiked 상태 전달이 올바르게 연결되어 있습니다. ImmutableList 사용과 key = { it.id } 설정으로 리스트 성능 최적화도 잘 되어 있습니다.

ViewModel의 onLikeToggle과의 연동도 자연스럽게 이루어져 있어 낙관적 업데이트가 UI에 즉시 반영될 것입니다.


51-149: Preview 구성이 훌륭합니다!

빈 목록, 단일 항목, 다중 항목 케이스를 모두 커버하고 있으며, isLikedisSolved의 다양한 조합을 테스트할 수 있도록 구성되어 있습니다. 이는 디자인 QA와 개발 중 빠른 확인에 유용합니다.

data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/RemoteQuestionDataSource.kt (1)

5-10: 인터페이스 설계가 깔끔합니다! 👍

좋아요 기능을 위한 addLikeremoveLike 메서드가 명확하게 정의되어 있습니다. 카테고리/검색 메서드를 제거하고 클라이언트 필터링으로 전환한 것도 적절한 설계 결정입니다.

다만 한 가지 고려해볼 점은, addLikeremoveLikeUnit을 반환하는데, 실패 시 예외를 던지는 방식보다 Result 타입을 반환하면 호출부에서 에러 처리가 더 명시적일 수 있습니다. 현재 구조도 충분히 동작하지만, 향후 에러 핸들링이 복잡해질 경우 참고해 주세요.

data/src/commonMain/kotlin/com/peto/droidmorning/data/datasource/question/remote/DefaultRemoteQuestionDataSource.kt (2)

42-49: 상수 정의가 잘 정리되어 있습니다! 👍

RPC 함수명과 테이블/컬럼명을 companion object 내 상수로 관리하여 유지보수성이 좋습니다. 스키마 변경 시 한 곳만 수정하면 되므로 실수를 줄일 수 있습니다.


24-29: 이미 Repository 레벨에서 runCatching을 통한 에러 처리가 구현되어 있습니다.

DefaultQuestionRepository.toggleQuestionLike()에서 전체 작업을 runCatching { } 블록으로 감싸고 있어, addLike()에서 발생할 수 있는 중복 제약 조건 에러가 이미 처리되고 있습니다. 반환되는 Result<Boolean>을 통해 에러가 도메인 레벨로 적절히 변환되고, ViewModel의 onFailure { } 블록에서 낙관적 업데이트를 롤백하며 사용자에게 에러를 알리는 구조로 잘 설계되어 있습니다.

Likely an incorrect or invalid review comment.

composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/component/QuestionFilterChips.kt (2)

26-34: 파라미터 리네이밍이 일관되게 적용되었습니다! 👍

showFavoritesOnlyshowLikedOnly, onFavoritesFilterToggleonLikedFilterToggle로 변경하여 PR 전체의 용어 통일 목표에 부합합니다. Kotlin의 네이밍 컨벤션에도 잘 맞습니다.

참고로 Line 65의 리소스 문자열 question_filter_favorites는 사용자에게 보이는 텍스트이므로 내부 API명과 다르게 유지하는 것이 의도된 것으로 보입니다. 만약 UI 텍스트도 "좋아요"로 변경해야 한다면 리소스 파일도 함께 업데이트해 주세요.


78-106: Preview 함수들이 잘 업데이트되었습니다.

선택/미선택 상태 모두 커버하는 프리뷰가 있어 개발 중 UI 확인이 용이합니다. 새 파라미터명이 정확히 적용되어 있습니다.

designsystem/src/commonMain/kotlin/com/peto/droidmorning/designsystem/component/QuestionCard.kt (2)

44-52: QuestionCard API가 잘 리팩토링되었습니다! 👍

isFavoriteisLiked, onFavoriteClickonLikeClick로 변경되어 좋아요 기능의 의미가 더 명확해졌습니다. 파라미터명이 실제 동작(좋아요 토글)과 잘 매칭됩니다.


94-111: 좋아요 상태에 따른 시각적 피드백이 잘 구현되어 있습니다.

  • 좋아요 상태: 채워진 별(Filled.Star) + Warning 색상
  • 미좋아요 상태: 빈 별(Outlined.Star) + MutedForeground 색상

직관적인 UI/UX입니다. Content description 리소스가 여전히 favorite_* 형태인데, 접근성 측면에서 사용자에게 노출되는 텍스트이므로 필요시 별도로 검토해 주세요.

data/src/commonTest/kotlin/com/peto/droidmorning/data/fake/FakeRemoteQuestionDataSource.kt (1)

6-25: 테스트 Fake 구현이 잘 되어 있습니다! 👍

mutableSetOf<Long>을 사용하여 좋아요 상태를 O(1) 시간복잡도로 추적하는 것은 효율적인 선택입니다. isLiked()clearLikes() 헬퍼 메서드는 테스트 검증과 setup/teardown에 유용합니다.

설계상 fetchQuestions()는 생성자에서 받은 질문 데이터를 반환하고, 좋아요 상태는 addLike()/removeLike()로 독립적으로 관리되도록 분리되어 있습니다. DefaultQuestionRepositoryTest에서 실제로 이 패턴을 따르고 있으며, 테스트가 isLiked() 헬퍼로 상태를 검증하는 방식도 올바릅니다. 관심사의 분리(separation of concerns)가 명확하게 구현되어 있습니다.

data/src/commonTest/kotlin/com/peto/droidmorning/data/repository/DefaultQuestionRepositoryTest.kt (1)

29-45: 좋아요 추가 테스트가 잘 구성되어 있습니다! 👍

given-when-then 패턴을 준수하며, Result 성공 여부와 실제 상태 변경을 fakeDataSource.isLiked()로 분리하여 검증하는 것이 좋습니다. Fake 객체의 공개 API를 통해 내부 상태를 검증하는 방식은 테스트의 유지보수성을 높여줍니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +85 to 101
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)
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd QuestionViewModel.kt

Repository: chanho0908/DroidMorning

Length of output: 154


🏁 Script executed:

# Find the QuestionUiState class
rg "class QuestionUiState|data class QuestionUiState" -A 20

Repository: chanho0908/DroidMorning

Length of output: 2691


🏁 Script executed:

# Search for toggleQuestionLike implementation
rg "fun toggleQuestionLike" -B 2 -A 15

Repository: chanho0908/DroidMorning

Length of output: 4874


필터링된 목록에서만 검색하므로 특정 상황에서 좋아요 토글이 무시될 수 있습니다.

낙관적 업데이트 패턴과 실패 시 롤백 처리가 잘 구현되었습니다. toggleQuestionLike는 원본 데이터인 allQuestions를 업데이트하고, filteredQuestions는 계산 속성(computed property)으로서 자동으로 변경사항을 반영하므로 상태 동기화 문제는 없습니다.

다만 하나의 중요한 문제가 있습니다:

Line 87의 검색 범위 문제: filteredQuestions에서만 질문을 찾고 있어, 현재 활성화된 필터에 의해 숨겨진 질문은 좋아요 토글이 실패합니다. 예를 들어 사용자가 "좋아요한 문제만" 필터 상태에서 좋아요 중인 문제를 토글하려고 하면, 해당 질문이 filteredQuestions에 포함되지 않아 작업이 무시됩니다.

개선 방안: _uiState.value.allQuestions에서 검색하거나, allQuestions 접근을 위한 공개 속성을 추가하는 것을 고려해볼 수 있습니다.

성공 시 loadQuestions() 호출로 서버와 재동기화할 필요는 없어 보입니다. 현재 구현은 낙관적 업데이트의 장점(빠른 응답)을 유지하면서 필요시에만 롤백하므로 효율적입니다.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/com/peto/droidmorning/question/vm/QuestionViewModel.kt`
around lines 85 - 101, The onLikeToggle function currently looks up the question
in _uiState.value.filteredQuestions which causes toggles to be ignored for items
hidden by filters; change the lookup to search the full list (e.g.,
_uiState.value.allQuestions.find { it.id == questionId } or expose an
allQuestions accessor) so the optimistic update and rollback (uses
toggleQuestionLike) always operate on the canonical item, leaving the rest of
the flow (calling _uiState.update { it.toggleQuestionLike(questionId) },
repository.toggleQuestionLike(...) and the onFailure rollback/sendUiEvent)
unchanged.

`DefaultRemoteQuestionDataSource` 클래스 내에서 사용자의 UID를 가져오는 방식을 프로퍼티에서 메서드로 변경했습니다.

기존에는 클래스 초기화 시점에 UID를 한 번만 가져와 프로퍼티에 저장했기 때문에, 사용자의 로그인 상태가 변경될 경우 오래된 UID 값을 사용할 수 있는 문제가 있었습니다.

이제 `uid()` 메서드를 통해 각 함수가 호출되는 시점의 최신 사용자 세션에서 UID를 가져오도록 수정하여, 데이터 요청 시 항상 정확하고 유효한 사용자 ID를 사용하도록 보장합니다.
@chanho0908 chanho0908 merged commit 61f40c6 into main Jan 15, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

질문 좋아요 기능 구현

1 participant