-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 서재UI/UX 개편 (10) - 계층 간 상태 동기화 및 코드 정리 #734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Paging 매핑을 위한 커스텀 페이징소스 구현 - 패키지 분리
|
@coderabbitai code review |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough이 PR은 서재(라이브러리) 기능의 계층 간 상태 동기화 및 코드 정리를 중심으로 대규모 리팩터링을 수행합니다. 필터, 정렬, 평점, 관심 작품 등 도메인 모델과 UI 모델을 일관성 있게 재정의하고, 기존의 복잡한 데이터 변환 및 매핑 로직을 단순화합니다. 또한, 불필요한 엔티티, DAO, UI 모델, 매퍼 파일 등이 제거되고, Paging 및 Room 관련 의존성 관리가 개선되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI
participant ViewModel
participant Repository
participant LocalDataSource
participant RemoteDataSource
UI->>ViewModel: 필터/정렬/평점 등 변경 이벤트
ViewModel->>Repository: updateFilter()/getNovels() 등 호출
Repository->>LocalDataSource: selectAllNovels()/insertNovels()/deleteAllNovels()
Repository->>RemoteDataSource: getUserNovels() (필터 파라미터 전달)
RemoteDataSource-->>Repository: UserNovelsEntity 결과 반환
LocalDataSource-->>Repository: PagingSource/PagingData 반환
Repository-->>ViewModel: PagingData<NovelEntity> 반환
ViewModel-->>UI: NovelUiModel 변환 후 UI 상태 업데이트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 Assessment against linked issues
Assessment against linked issues: Out-of-scope changes(해당 이슈(#713) 목적과 무관한 변경사항 없음) Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@s9hn I'll review the changes in this PR focusing on the bookshelf UI/UX redesign, inter-layer state synchronization, and code cleanup improvements. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Nitpick comments (8)
gradle/libs.versions.toml (1)
35-36: 페이징 버전 중앙 관리 👍
paging = "3.3.6"버전 추가로 버전 일관성이 확보되었습니다.
추가적으로 JVM 단위 테스트에서androidx.paging:paging-common이 필요할 수 있으니 사용 여부를 확인하세요.data/library/build.gradle.kts (1)
14-15: 버전 카탈로그 활용으로 의존성 간소화하드코딩된 버전 문자열 제거 후
libs.paging.runtime사용으로 유지보수성이 향상되었습니다.
만약 JVM 테스트 코드에서 코루틴 기반paging-common이 필요하다면 동일하게implementation(libs.paging.common)(alias 추가 필요) 도 고려해주세요.core/database/build.gradle.kts (1)
15-17:libs.paging.runtime중복 여부 확인 필요
libs.room.paging이 Room-Paging 모듈을 이미 포함하므로,libs.paging.runtime을 추가하면 바이너리 중복·메서드 수 증가 가능성이 있습니다. 실제 필요 여부를 확인해 중복 의존을 최소화하세요.core/database/src/main/java/com/into/websoso/core/database/Converters.kt (1)
6-10: 공백 트리밍 여부 검토
fromString에서" a , b "같은 입력은 공백을 포함한" a "등을 그대로 보존합니다.
저장·표시 단계에서 문제가 없다면 괜찮지만, 일관된 값 비교를 요구한다면trim()처리 추가가 필요합니다.core/database/src/main/java/com/into/websoso/core/database/datasource/library/mapper/NovelMapper.kt (1)
6-22: index 파라미터에 대한 유효성 검증 추가 고려
toNovelDatabase함수의index파라미터가 음수가 될 수 있는 경우를 처리하는 것이 좋습니다. 데이터베이스의 정렬 인덱스로 사용되므로 음수 값이 의도하지 않은 정렬 순서를 야기할 수 있습니다.internal fun NovelEntity.toNovelDatabase(index: Int): InDatabaseNovelEntity = + require(index >= 0) { "Sort index must be non-negative: $index" } InDatabaseNovelEntity( userNovelId = userNovelId, novelId = novelId,data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt (1)
50-70: 필터 파라미터 매핑 로직이 적절합니다.
LibraryFilter에서 원격 데이터 소스 파라미터로의 변환 로직이 잘 구현되었습니다:
- 빈 리스트/기본값일 때 null로 설정하는 조건부 처리가 적절함
runCatching을 통한 예외 처리도 좋음다만
MyLibraryRepository와 거의 동일한getUserNovels함수가 존재하는 것으로 보입니다. 공통 로직을 별도 유틸리티 함수로 추출하는 것을 고려해보세요:// 공통 유틸리티 함수 예시 internal object LibraryFilterMapper { suspend fun fetchUserNovels( userId: Long, lastUserNovelId: Long, libraryFilter: LibraryFilter, remoteDataSource: LibraryRemoteDataSource ) = runCatching { remoteDataSource.getUserNovels( userId = userId, lastUserNovelId = lastUserNovelId, size = PAGE_SIZE, // ... 나머지 매핑 로직 ) } }domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatuses.kt (1)
17-23: 메서드 이름이 실제 동작과 다릅니다.
set메서드는 실제로는 상태를 토글하는 동작을 수행하지만, 이름만으로는 값을 설정하는 것처럼 보입니다.toggle또는toggleStatus와 같은 더 명확한 이름을 사용하는 것이 좋겠습니다.- fun set(readStatus: ReadStatus): ReadStatuses { + fun toggle(readStatus: ReadStatus): ReadStatuses {domain/library/src/main/java/com/into/websoso/domain/library/model/NovelRating.kt (1)
13-18: 메서드 이름이 실제 동작과 일치하지 않습니다.
set메서드는 실제로는 rating을 토글하는 동작을 수행합니다 (동일한 rating이면 DEFAULT로, 다르면 새 값으로).toggle또는toggleRating과 같은 더 명확한 이름을 사용하는 것이 좋겠습니다.- fun set(newRating: Rating): NovelRating = + fun toggle(newRating: Rating): NovelRating =
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (61)
app/src/main/java/com/into/websoso/ui/splash/SplashViewModel.kt(2 hunks)core/database/build.gradle.kts(1 hunks)core/database/src/main/java/com/into/websoso/core/database/Converters.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/WebsosoDatabase.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/dao/FilteredNovelDao.kt(0 hunks)core/database/src/main/java/com/into/websoso/core/database/datasource/library/DefaultLibraryLocalDataSource.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/datasource/library/dao/NovelDao.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/datasource/library/entity/InDatabaseNovelEntity.kt(2 hunks)core/database/src/main/java/com/into/websoso/core/database/datasource/library/mapper/NovelMapper.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/datasource/library/paging/MapValuePagingSource.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/di/DatabaseModule.kt(1 hunks)core/database/src/main/java/com/into/websoso/core/database/entity/InDatabaseFilteredNovelEntity.kt(0 hunks)core/datastore/src/main/java/com/into/websoso/core/datastore/datasource/library/DefaultLibraryFilterDataSource.kt(1 hunks)core/datastore/src/main/java/com/into/websoso/core/datastore/datasource/library/model/LibraryFilterPreferences.kt(1 hunks)data/library/build.gradle.kts(1 hunks)data/library/src/main/java/com/into/websoso/data/filter/FilterRepository.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/filter/datasource/LibraryFilterLocalDataSource.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/filter/model/LibraryFilter.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/filter/repository/MyLibraryFilterRepository.kt(2 hunks)data/library/src/main/java/com/into/websoso/data/filter/repository/UserLibraryFilterRepository.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/library/datasource/FilteredLibraryLocalDataSource.kt(0 hunks)data/library/src/main/java/com/into/websoso/data/library/datasource/LibraryLocalDataSource.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/library/model/NovelEntity.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/library/paging/LibraryPagingSource.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/library/paging/LibraryRemoteMediator.kt(1 hunks)data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt(3 hunks)data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt(2 hunks)domain/library/build.gradle.kts(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/AttractivePoint.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/AttractivePoints.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/NovelRating.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/Rating.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatus.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatuses.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/SortCriteria.kt(1 hunks)domain/library/src/main/java/com/into/websoso/domain/library/model/SortType.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt(5 hunks)feature/library/src/main/java/com/into/websoso/feature/library/LibraryViewModel.kt(5 hunks)feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryGridList.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryGridListItem.kt(7 hunks)feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryList.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryListItem.kt(11 hunks)feature/library/src/main/java/com/into/websoso/feature/library/component/LibrayFilterTopBar.kt(11 hunks)feature/library/src/main/java/com/into/websoso/feature/library/filter/LibraryFilterBottomSheetScreen.kt(3 hunks)feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetAttractivePoints.kt(3 hunks)feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetNovelRatingGrid.kt(6 hunks)feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetReadStatus.kt(3 hunks)feature/library/src/main/java/com/into/websoso/feature/library/mapper/AttractivePointsMapper.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/mapper/ListItemMapper.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/mapper/NovelMapper.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/AttractivePointUiModel.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryFilterType.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryFilterUiModel.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryListItemModel.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryUiState.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/NovelUiModel.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/RatingLevelUiModel.kt(0 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/RatingStarUiModel.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/ReadStatusUiModel.kt(1 hunks)feature/library/src/main/java/com/into/websoso/feature/library/model/SortTypeUiModel.kt(0 hunks)gradle/libs.versions.toml(2 hunks)
💤 Files with no reviewable changes (11)
- feature/library/src/main/java/com/into/websoso/feature/library/mapper/AttractivePointsMapper.kt
- domain/library/src/main/java/com/into/websoso/domain/library/model/SortType.kt
- feature/library/src/main/java/com/into/websoso/feature/library/model/AttractivePointUiModel.kt
- feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryFilterType.kt
- feature/library/src/main/java/com/into/websoso/feature/library/model/RatingLevelUiModel.kt
- feature/library/src/main/java/com/into/websoso/feature/library/model/SortTypeUiModel.kt
- core/database/src/main/java/com/into/websoso/core/database/entity/InDatabaseFilteredNovelEntity.kt
- feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryListItemModel.kt
- feature/library/src/main/java/com/into/websoso/feature/library/mapper/ListItemMapper.kt
- core/database/src/main/java/com/into/websoso/core/database/dao/FilteredNovelDao.kt
- data/library/src/main/java/com/into/websoso/data/library/datasource/FilteredLibraryLocalDataSource.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: `bottomsheetstate.show()`는 바텀시트 애니메이션이 완료될 때까지 suspend하는 함수이므로, `invokeoncompletion`에서 실행되는 코드는 바텀시트...
Learnt from: s9hn
PR: Team-WSS/WSS-Android#728
File: feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt:77-84
Timestamp: 2025-07-24T09:02:01.000Z
Learning: `bottomSheetState.show()`는 바텀시트 애니메이션이 완료될 때까지 suspend하는 함수이므로, `invokeOnCompletion`에서 실행되는 코드는 바텀시트가 완전히 표시된 후에 실행된다.
Applied to files:
app/src/main/java/com/into/websoso/ui/splash/SplashViewModel.kt
🧬 Code Graph Analysis (8)
domain/library/build.gradle.kts (1)
build-logic/src/main/kotlin/com/into/websoso/WebsosoDependenciesExtensions.kt (1)
implementation(70-76)
data/library/build.gradle.kts (1)
build-logic/src/main/kotlin/com/into/websoso/WebsosoDependenciesExtensions.kt (1)
implementation(70-76)
core/database/build.gradle.kts (1)
build-logic/src/main/kotlin/com/into/websoso/WebsosoDependenciesExtensions.kt (1)
implementation(70-76)
data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt (1)
data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt (1)
getUserNovels(46-66)
data/library/src/main/java/com/into/websoso/data/library/paging/LibraryPagingSource.kt (1)
data/library/src/main/java/com/into/websoso/data/library/paging/LibraryRemoteMediator.kt (1)
getNovels(15-46)
feature/library/src/main/java/com/into/websoso/feature/library/model/NovelUiModel.kt (1)
core/common/src/main/java/com/into/websoso/core/common/extensions/DateFormatter.kt (1)
formatDateRange(10-30)
feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetAttractivePoints.kt (1)
feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetClickableItem.kt (1)
LibraryFilterBottomSheetClickableItem(27-56)
data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt (1)
data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt (1)
getUserNovels(50-70)
🔇 Additional comments (94)
domain/library/build.gradle.kts (1)
12-12: 공통 유틸 추가는 적절합니다
implementation(projects.core.common)추가로 공통 코드 재사용성이 높아졌습니다. 별도 이슈 없습니다.gradle/libs.versions.toml (1)
102-104: 라이브러리 alias 정의 일관성 확보
room-paging,paging-compose,paging-runtime를 버전 카탈로그로 이동한 것은 좋은 개선입니다. 별도 문제 없습니다.core/database/build.gradle.kts (1)
18-23: Room 관련 의존성 설정 적절
room.ktx,room.runtime,kapt(room.compiler)구성이 표준 패턴을 따릅니다. 문제 없습니다.domain/library/src/main/java/com/into/websoso/domain/library/model/SortCriteria.kt (1)
11-13:entries프로퍼티 사용 시 Kotlin 버전 의존성 확인 필요
entries는 Kotlin 1.9 이상에서 정식 지원됩니다.
모듈의 Kotlin 버전이 이를 보장하는지 CI 설정을 확인해 주세요.core/database/src/main/java/com/into/websoso/core/database/Converters.kt (1)
5-5:internal변경이 Room 외부 모듈 사용 시 컴파일 오류를 유발할 수 있습니다Room @TypeConverter 클래스를 다른 모듈에서 참조한다면
internal로 제한할 경우 KAPT 단계에서 접근 불가 오류가 발생합니다.
현재 Converters가 오직 동일 모듈 내@Database에서만 사용되는지 다시 한 번 확인해 주세요.feature/library/src/main/java/com/into/websoso/feature/library/model/RatingStarUiModel.kt (1)
3-7: 간결하고 명확합니다별점 상태 표현을 enum으로 분리한 결정이 UI 가독성과 유지보수성을 향상시킵니다. 추가 의견 없습니다.
data/library/src/main/java/com/into/websoso/data/filter/datasource/LibraryFilterLocalDataSource.kt (1)
9-9: 파라미터 명 변경이 일관성 있게 반영되었습니다
params→libraryFilter로 변경해 의미가 명확해졌습니다. 관련 호출부 전반이 모두 갱신되었는지만 한번 더 확인해 주세요.core/datastore/src/main/java/com/into/websoso/core/datastore/datasource/library/DefaultLibraryFilterDataSource.kt (1)
45-49: 이름 변경 반영 확인 완료메서드 시그니처 및 내부 로직이 새 파라미터 명(
libraryFilter)을 일관되게 사용합니다. 직렬화 매퍼(toPreferences) 호출도 문제없습니다.core/datastore/src/main/java/com/into/websoso/core/datastore/datasource/library/model/LibraryFilterPreferences.kt (1)
9-10: 일관성 검증 완료: Map<String, Boolean> 패턴 제거됨전체 레이어에서
readStatuses및attractivePoints가List<String>으로 일관되게 변경되었고, 더 이상Map<String, Boolean>사용 흔적이 없습니다.
해당 변경사항을 승인합니다.core/database/src/main/java/com/into/websoso/core/database/datasource/library/entity/InDatabaseNovelEntity.kt (3)
1-1: 패키지 구조 개선으로 모듈 책임 분리가 잘 이루어졌습니다.
datasource.library.entity패키지로 이동하여 데이터베이스 엔티티의 위치가 더욱 명확해졌습니다.
7-7: 내부 가시성으로 캡슐화가 개선되었습니다.엔티티 클래스를
internal로 선언하여 데이터베이스 모듈 내부로 적절히 캡슐화했습니다. 이는 DB 의존성을 데이터 레이어로부터 분리하는 목표와 일치합니다.
11-11: 정렬 기능을 위한 sortIndex 필드 추가가 적절합니다.소설 목록의 정렬을 지원하기 위한
sortIndex필드가 추가되어 사용자 정의 순서를 유지할 수 있게 되었습니다.domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatus.kt (3)
5-5: 한국어 라벨 추가로 사용자 경험이 개선되었습니다.
label프로퍼티를 추가하여 UI에서 한국어 표시를 지원할 수 있게 되었습니다.
7-9: 읽기 상태별 한국어 라벨이 적절하게 정의되었습니다."보는중", "봤어요", "하차함"으로 사용자 친화적인 한국어 표현을 사용했습니다.
13-13: null 반환으로 안전한 타입 변환을 구현했습니다.예외를 던지는 대신
ReadStatus?를 반환하도록 변경하여 런타임 크래시를 방지하고 더 안전한 에러 처리가 가능해졌습니다.core/database/src/main/java/com/into/websoso/core/database/di/DatabaseModule.kt (1)
13-27: 데이터베이스 의존성 주입 모듈이 잘 구현되었습니다.Hilt 모듈을 별도로 분리하여 데이터베이스 인스턴스 제공을 중앙화했습니다. 싱글톤 스코프와 적절한 애노테이션 사용으로 모범 사례를 따르고 있습니다.
domain/library/src/main/java/com/into/websoso/domain/library/model/AttractivePoint.kt (2)
3-12: 매력 포인트 도메인 모델이 잘 설계되었습니다.한국어 라벨과 영어 키를 분리하여 UI 표시와 API 통신에 적합한 구조로 구현했습니다. "세계관", "소재", "캐릭터", "관계", "분위기" 등 소설의 주요 매력 요소들을 포괄적으로 정의했습니다.
14-16: 안전한 키 조회 메서드가 일관성 있게 구현되었습니다.
ReadStatusenum과 동일한 패턴으로 nullable 반환 타입을 사용하여 일관성과 안전성을 확보했습니다.feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetReadStatus.kt (3)
25-25: 타입 안전성 향상을 위한 도메인 모델 적용 승인
Map<ReadStatus, Boolean>에서ReadStatuses데이터 클래스로의 변경이 좋습니다. 이는 도메인 모델 중심 설계로의 전환을 잘 보여주며, 타입 안전성과 코드 가독성을 향상시킵니다.
39-39: 인덱스 접근 방식 단순화 승인
ReadStatuses데이터 클래스의 인덱스 연산자를 직접 사용하여 코드가 더욱 간결해졌습니다. null 체크가 필요 없어져 안전하고 명확한 접근이 가능합니다.Also applies to: 47-47, 55-55
65-65: 프리뷰 함수 일관성 유지 승인프리뷰 함수가 새로운
ReadStatuses()인스턴스를 사용하도록 적절히 업데이트되었습니다.domain/library/src/main/java/com/into/websoso/domain/library/model/Rating.kt (2)
5-13: 평점 도메인 모델 설계 승인평점 값들을 enum으로 정의하여 타입 안전성을 확보한 설계가 우수합니다. 실제 사용되는 평점 값들(3.5, 4.0, 4.5, 4.8)을 명시적으로 정의하고 DEFAULT 값으로 0.0을 제공하여 예외 상황을 처리한 점이 좋습니다.
15-17: 타입 변환 로직 구현 승인
from함수에서isCloseTo확장 함수를 사용한 근사 비교를 통해 Float 값을 Rating enum으로 안전하게 변환하는 방식이 적절합니다. 매칭되지 않는 값에 대해 DEFAULT를 반환하는 안전한 처리도 잘 구현되었습니다.feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryList.kt (1)
16-16: UI 모델 통합을 위한 타입 업데이트 승인
LibraryListItemModel에서NovelUiModel로의 타입 변경이 적절합니다. 이는 UI 모델 통합 리팩터링의 일환으로 코드베이스 전반의 일관성을 향상시키며, 도메인 중심 설계로의 전환을 잘 반영합니다.Also applies to: 18-18
feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryGridList.kt (1)
21-21: 일관된 UI 모델 통합 적용 승인그리드 리스트 컴포넌트에서도
NovelUiModel로의 타입 변경이 일관되게 적용되었습니다. 리스트와 그리드 컴포넌트 간의 타입 일관성이 유지되어 코드베이스의 통일성이 향상되었습니다.Also applies to: 24-24
core/database/src/main/java/com/into/websoso/core/database/datasource/library/mapper/NovelMapper.kt (1)
1-39: 클린 아키텍처 원칙을 잘 따른 매퍼 구현데이터베이스 레이어와 데이터 레이어 간의 변환을 담당하는 매퍼 함수들이 적절하게 구현되었습니다.
sortIndex를 데이터베이스 레이어에서만 관리하도록 한 설계가 좋습니다.data/library/src/main/java/com/into/websoso/data/library/datasource/LibraryLocalDataSource.kt (1)
1-12: 데이터베이스 의존성 분리가 잘 구현됨인터페이스가 데이터베이스 구현 세부사항으로부터 완전히 분리되어 클린 아키텍처 원칙을 잘 따르고 있습니다.
PagingSource가 도메인 엔티티(NovelEntity)를 반환하도록 변경된 것이 특히 좋은 개선입니다.data/library/src/main/java/com/into/websoso/data/filter/repository/UserLibraryFilterRepository.kt (2)
18-35: 필터 파라미터 타입 단순화가 잘 구현됨
Map<String, Boolean>에서List<String>으로의 변경은 선택된 항목만 저장하여 데이터 모델을 단순화합니다. 이는 메모리 효율성과 코드 가독성 측면에서 좋은 개선입니다.
12-36: 메모리 내 상태 관리 전략 확인 필요
UserLibraryFilterRepository는MutableStateFlow를 사용하여 메모리에만 상태를 저장하는 반면,MyLibraryFilterRepository는 로컬 데이터 소스를 통해 영속성을 제공합니다. 이 차이가 의도적인 설계인지 확인이 필요합니다.사용자 라이브러리 필터가 앱 재시작 시 초기화되어도 괜찮은지, 아니면 영속성이 필요한지 검토해 주세요.
data/library/src/main/java/com/into/websoso/data/filter/repository/MyLibraryFilterRepository.kt (1)
17-40: 로컬 데이터 소스를 활용한 영속성 구현이 우수함
distinctUntilChanged()를 사용하여 불필요한 상태 업데이트를 방지하고, 명명된 파라미터(libraryFilter)를 사용하여 코드 가독성을 향상시킨 점이 좋습니다.feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryUiState.kt (1)
1-8: UI 상태 모델 단순화가 효과적으로 구현됨
LibraryFilterUiState를LibraryFilterUiModel로 교체하여 도메인 모델 중심의 아키텍처로 전환한 것이 좋습니다. 이는 타입 안전성을 높이고 레이어 간 책임을 명확히 분리합니다.data/library/src/main/java/com/into/websoso/data/filter/FilterRepository.kt (1)
9-15: updateFilter 메서드 시그니처 일관성 확인 완료인터페이스와 두 구현체(MyLibraryFilterRepository, UserLibraryFilterRepository) 모두
List<String>?기반 파라미터로 동일하게 변경되어 있어 추가 검토나 수정이 필요 없습니다.feature/library/src/main/java/com/into/websoso/feature/library/model/ReadStatusUiModel.kt (2)
9-16: 도메인 모델 중심으로의 리팩터링이 훌륭합니다.문자열 기반
key/label방식에서ReadStatus도메인 모델을 직접 참조하는 방식으로 변경되어 타입 안전성과 코드 명확성이 크게 개선되었습니다.
19-19: null 안전성 처리가 적절합니다.
from메서드에서 nullableReadStatus를 받아서 안전하게 매칭하는 로직이 잘 구현되었습니다. 매칭되지 않는 경우 null을 반환하는 것도 적절합니다.data/library/src/main/java/com/into/websoso/data/library/model/NovelEntity.kt (1)
9-23: NovelEntity 변환 로직이 전용 매퍼 파일로 올바르게 이동되었습니다.아래 파일들에서 매핑 함수들이 확인되었습니다:
- core/database/src/main/java/com/into/websoso/core/database/datasource/library/mapper/NovelMapper.kt
•NovelEntity.toNovelDatabase()/InDatabaseNovelEntity.toData()- core/network/src/main/java/com/into/websoso/core/network/datasource/library/model/response/NovelResponseDto.kt
•toData(): NovelEntity- app/src/main/java/com/into/websoso/domain/mapper/NovelMapper.kt
•NovelEntity.toDomain()- feature/library/src/main/java/com/into/websoso/feature/library/mapper/NovelMapper.kt
•NovelEntity.toUiModel()- app/src/main/java/com/into/websoso/data/mapper/NovelMapper.kt & UserNovelMapper.kt
- app/src/main/java/com/into/websoso/ui/mapper/NovelMapper.kt
변환 함수가 누락된 곳 없이 모두 전용 매퍼로 모듈화되었으므로 변경사항을 승인합니다.
feature/library/src/main/java/com/into/websoso/feature/library/mapper/NovelMapper.kt (1)
10-24: 매퍼 함수가 잘 구현되었습니다.도메인 모델 중심의 변환 로직이 명확하고 읽기 쉽게 작성되었습니다. 특히 다음 부분들이 인상적입니다:
userNovelRating의 조건부 처리 (0보다 클 때만 값 설정)- 도메인 모델의 팩토리 메서드들을 적절히 활용
- 모든 필드가 일관성 있게 매핑됨
data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt (1)
42-44: 람다 함수를 통한 캡슐화가 잘 구현되었습니다.페이징 소스 생성 로직에서 람다 함수를 사용하여 데이터 페칭 로직을 캡슐화한 것이 좋습니다. 이는 관심사 분리와 테스트 용이성을 향상시킵니다.
feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetNovelRatingGrid.kt (3)
24-29: 도메인 모델 사용이 적절합니다.UI 모델 대신 도메인 모델(
NovelRating,Rating)을 사용하도록 변경한 것은 계층 간 책임 분리를 명확하게 하는 좋은 접근입니다.
32-35: 함수 시그니처 개선이 잘 되었습니다.
NovelRating과Rating타입을 사용하여 타입 안정성이 향상되었고, 콜백 함수도 명확한 enum 타입을 받도록 개선되었습니다.
53-53: 선택 상태 로직이 개선되었습니다.
isCloseTo메서드를 사용하여 평점 비교 로직을 도메인 모델에 캡슐화한 것은 UI 레이어의 책임을 줄이고 유지보수성을 향상시킵니다.Also applies to: 62-62, 77-77, 86-86
core/database/src/main/java/com/into/websoso/core/database/datasource/library/dao/NovelDao.kt (2)
1-1: 데이터베이스 계층 캡슐화가 개선되었습니다.DAO를
internal로 제한하고 패키지 구조를 세분화한 것은 모듈 간 의존성을 명확히 하고 데이터베이스 접근을 적절히 제한하는 좋은 설계입니다.Also applies to: 17-17
21-28: 메서드 추가 및 이름 변경이 적절합니다.
selectNovelsCount()추가로 통계 기능 구현이 가능해졌고,deleteAllNovels()로의 이름 변경은 메서드의 목적을 더 명확히 합니다.data/library/src/main/java/com/into/websoso/data/library/paging/LibraryPagingSource.kt (1)
10-34: 의존성 주입 패턴이 개선되었습니다.고차 함수를 사용하여 데이터 소스 의존성을 제거하고 테스트 가능성을 높인 것은 훌륭한 리팩토링입니다.
Result.fold를 사용한 에러 처리도 깔끔합니다.domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatuses.kt (1)
26-30: ReadStatus.from 메서드가 정상적으로 존재함을 확인했습니다
domain/library/src/main/java/com/into/websoso/domain/library/model/ReadStatus.kt의 companion object에fun from(key: String): ReadStatus? = entries.find { it.key == key }가 정의되어 있어, 확장 함수
ReadStatus::from호출 시 문제가 없습니다. 추가 수정은 필요하지 않습니다.feature/library/src/main/java/com/into/websoso/feature/library/filter/component/LibraryFilterBottomSheetAttractivePoints.kt (2)
29-30: 타입 안전성이 개선되었습니다.
Map<AttractivePoints, Boolean>에서AttractivePoints도메인 모델로 변경하여 타입 안전성이 향상되었고, 콜백 파라미터도 개별AttractivePoint를 받도록 개선되어 더 명확한 API를 제공합니다.
42-76: 선택 상태 접근 방식이 일관성 있게 개선되었습니다.
AttractivePoints도메인 모델의 get 연산자를 직접 사용하여 각 매력 포인트의 선택 상태를 확인하는 방식이 깔끔하고 일관성 있습니다. 이전의 안전 호출 방식보다 더 직관적입니다.domain/library/src/main/java/com/into/websoso/domain/library/model/NovelRating.kt (1)
21-22: Rating.from 메서드 존재 확인 완료
domain/library/src/main/java/com/into/websoso/domain/library/model/Rating.kt의 companion object에fun from(value: Float): Rating = entries.find { it.value.isCloseTo(value) } ?: DEFAULT구현이 존재함을 확인했습니다. 따라서
NovelRating.from(value)에서 호출되는Rating.from(value)는 정상 동작합니다.
- Rating.from 구현 위치: domain/library/src/main/java/com/into/websoso/domain/library/model/Rating.kt (라인 16)
추가 조치가 필요 없습니다.
feature/library/src/main/java/com/into/websoso/feature/library/model/NovelUiModel.kt (2)
7-21: 도메인 모델을 활용한 깔끔한 UI 모델 설계입니다.UI 레이어에서 도메인 모델(
NovelRating,AttractivePoints)을 직접 사용하여 타입 안전성을 확보하고, 레이어 간 일관성을 유지한 좋은 설계입니다. 기본 파라미터로formattedDateRange를 계산하는 것도 효율적입니다.
24-36: 별점 계산 로직이 정확하고 효율적입니다.null 체크, 정수 부분과 소수 부분을 이용한 full/half 별 계산,
buildList를 사용한 효율적인 리스트 생성 등 모든 로직이 정확하게 구현되어 있습니다.data/library/src/main/java/com/into/websoso/data/filter/model/LibraryFilter.kt (2)
3-5: 상수를 최상위 레벨로 이동한 것이 좋은 개선입니다.companion object에서 최상위 상수로 이동하여 접근성이 향상되었고, 클래스 한정자 없이 사용할 수 있게 되었습니다.
11-12: 데이터 구조 단순화로 관심사 분리가 개선되었습니다.
Map<String, Boolean>에서List<String>으로 변경하여 데이터 레이어에서는 선택된 항목의 키만 저장하고, 도메인 레이어에서 선택 상태를 관리하는 구조로 관심사가 명확히 분리되었습니다. 이는 더 깔끔하고 유지보수하기 쉬운 설계입니다.core/database/src/main/java/com/into/websoso/core/database/datasource/library/DefaultLibraryLocalDataSource.kt (2)
18-37: 구현이 깔끔하고 책임이 명확하게 분리되어 있습니다.
DefaultLibraryLocalDataSource클래스의 구현이 잘 되어 있습니다. 오프셋 기반 인덱싱을 통한 소설 삽입, 페이징 소스를 통한 조회, 그리고 매핑 함수를 활용한 도메인 모델 변환이 적절하게 처리되고 있습니다.
39-45: Dagger Hilt 모듈 설정이 올바르게 구성되어 있습니다.
LibraryDataSourceModule이SingletonComponent에 올바르게 설치되어 있고,LibraryLocalDataSource인터페이스에 구현체를 적절하게 바인딩하고 있습니다.feature/library/src/main/java/com/into/websoso/feature/library/component/LibrayFilterTopBar.kt (3)
42-43: 도메인 모델을 직접 사용하는 것이 좋은 접근입니다.
SortCriteria도메인 모델과LibraryFilterUiModel을 직접 사용하여 UI와 도메인 계층 간의 일관성을 향상시켰습니다.
46-49: 콜백 시그니처 단순화가 코드를 더 깔끔하게 만들었습니다.
onFilterClick콜백이 매개변수 없는 함수로 단순화되어 호출하는 쪽에서 더 간단하게 사용할 수 있게 되었습니다.
81-121: 필터 칩 구현이 새로운 모델과 잘 통합되어 있습니다.
LibraryFilterUiModel의 계산된 속성들(readStatusLabelText,ratingText,attractivePointLabelText)을 활용하여 필터 상태를 올바르게 표시하고 있습니다. 각 필터의 선택 상태도 적절하게 반영되고 있습니다.feature/library/src/main/java/com/into/websoso/feature/library/model/LibraryFilterUiModel.kt (3)
8-14: 도메인 모델을 활용한 UI 모델 설계가 우수합니다.
LibraryFilterUiModel이 UI별 모델 대신 도메인 모델(SortCriteria,ReadStatuses,AttractivePoints,NovelRating)을 직접 사용하여 계층 간 일관성을 향상시켰습니다. 기본값들도 적절하게 설정되어 있습니다.
15-31: 계산된 속성들이 UI 표시 로직을 깔끔하게 캡슐화했습니다.
ratingText,readStatusLabelText,attractivePointLabelText,isFilterApplied속성들이 UI 컴포넌트에서 필요한 표시 로직을 적절하게 추상화하고 있습니다.
33-42: 라벨 생성 로직이 다양한 시나리오를 잘 처리합니다.
createLabel함수가 빈 목록, 단일 선택, 복수 선택 시나리오를 모두 적절하게 처리하여 사용자에게 명확한 정보를 제공합니다.data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt (2)
29-29: 데이터 소스 통합이 아키텍처를 단순화했습니다.
FilteredLibraryLocalDataSource대신LibraryLocalDataSource를 사용하여 데이터 계층의 복잡성을 줄이고 일관된 소설 엔티티 처리를 가능하게 했습니다.
37-44: 람다 기반 RemoteMediator 구성이 더 유연합니다.
LibraryRemoteMediator에 람다 함수들을 전달하는 방식이 명시적인 데이터 소스 전달보다 더 깔끔하고 유연한 구조를 제공합니다.domain/library/src/main/java/com/into/websoso/domain/library/model/AttractivePoints.kt (4)
3-5: enum에서 data class로의 리팩터링이 유연성을 크게 향상시켰습니다.Map 기반 저장소를 사용하여 각
AttractivePoint의 선택 상태를 독립적으로 관리할 수 있게 되었습니다. 기본값으로 모든 항목이 false로 초기화되는 것도 적절합니다.
6-17: 계산된 속성들이 깔끔한 API를 제공합니다.
isSelected,selectedAttractivePoints,selectedLabels,selectedKeys속성들이 선택된 항목들에 대한 다양한 뷰를 효율적으로 제공하여 UI 및 데이터 변환에서 활용하기 좋습니다.
18-26: 연산자 오버로딩과 불변 업데이트 패턴이 우수합니다.
operator fun get과set함수가 직관적인 사용법을 제공하며,set함수가 새로운 인스턴스를 반환하는 불변 업데이트 패턴을 따르고 있어 함수형 프로그래밍 접근법에 적합합니다.
29-34: 확장 함수를 통한 변환 로직이 편리합니다.
List<String>.toAttractivePoints()확장 함수가 문자열 리스트를AttractivePoints인스턴스로 변환하는 편리한 방법을 제공합니다.AttractivePoint::from을 활용한 안전한 변환도 좋습니다.feature/library/src/main/java/com/into/websoso/feature/library/filter/LibraryFilterBottomSheetScreen.kt (3)
22-33: 도메인 모델로의 성공적인 전환을 승인합니다UI 레이어에서 도메인 모델(
AttractivePoint,NovelRating,Rating,ReadStatuses)을 직접 사용하는 것은 계층 간 상태 동기화를 개선하는 훌륭한 접근입니다. 이는 타입 안정성을 높이고 데이터 변환 로직을 단순화합니다.
38-58: 콜백 시그니처 단순화가 잘 이루어졌습니다함수 파라미터들이 도메인 모델 타입으로 일관성 있게 변경되었으며, 특히
LibraryFilterUiModel을 통한 상태 전달이 깔끔하게 구현되었습니다. 이는 필터 상태 관리를 중앙화하는 좋은 설계입니다.
146-146: 프리뷰 함수가 새 모델과 일치하게 업데이트되었습니다
LibraryFilterUiModel()의 기본 생성자 사용으로 프리뷰가 올바르게 작동할 것입니다.data/library/src/main/java/com/into/websoso/data/library/paging/LibraryRemoteMediator.kt (3)
16-20: 의존성 주입에서 고차 함수로의 전환을 승인합니다데이터소스 직접 의존성을 제거하고 suspend 함수들을 주입받는 방식은 훌륭한 아키텍처 개선입니다. 이는 결합도를 낮추고 테스트 가능성을 크게 향상시킵니다.
31-41: Result 타입을 활용한 에러 처리 개선
Result.fold를 사용한 에러 처리는 함수형 프로그래밍 접근법으로 코드의 가독성과 안정성을 모두 향상시킵니다. 성공/실패 케이스가 명확하게 분리되어 있습니다.
12-12: 제네릭 타입 NovelEntity 적용 검증 완료
InDatabaseFilteredNovelEntity 참조가 모두 제거되었고, Repository(MyLibraryRepository, UserLibraryRepository), LocalDataSource, RemoteMediator 등 주요 컴포넌트에서 NovelEntity가 일관되게 사용되는 것을 확인했습니다. 추가 조치 불필요합니다.feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryGridListItem.kt (3)
39-41: UI 모델 타입 업데이트가 성공적으로 완료되었습니다
NovelUiModel과RatingStarUiModel로의 전환은 도메인 중심 설계와 일치하며, 타입 안정성을 향상시킵니다. 기존 UI 로직은 그대로 유지하면서 모델만 개선한 것이 좋습니다.
110-110: 파라미터 이름 개선
readStatus에서readStatusUiModel로 변경한 것은 타입을 더 명확하게 표현하여 코드 가독성을 향상시킵니다.
189-196: 열거형 값 매핑 확인
RatingStarUiModel의 새로운 열거형 값들(FULL,HALF,EMPTY)이 올바르게 아이콘 리소스와 매핑되었습니다. 명명이 더 직관적이고 이해하기 쉽습니다.feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt (3)
33-46: 도메인 모델 import 및 타입 일관성 확인
AttractivePoint,Rating,NovelUiModel등 도메인 모델들이 일관성 있게 import되고 사용되었습니다. 이는 계층 간 명확한 분리를 보여줍니다.
122-141: 콜백 시그니처 단순화 성공
onFilterClick에서LibraryFilterType파라미터 제거와 도메인 모델 타입 사용(AttractivePoint,Rating)은 인터페이스를 크게 단순화시켰습니다. 이는 유지보수성을 향상시키는 좋은 변경입니다.
156-170: 필터 상태 접근 패턴 개선
uiState.libraryFilterUiModel을 통한 필터 상태 접근은 상태 관리를 중앙화하고 일관성을 보장합니다.isFilterApplied속성 사용도 명확하고 직관적입니다.feature/library/src/main/java/com/into/websoso/feature/library/LibraryViewModel.kt (5)
10-17: 도메인 모델 import 및 확장 함수 사용 승인도메인 모델과 확장 함수들(
toAttractivePoints,toReadStatuses)의 import는 데이터 변환 로직을 도메인 레이어로 캡슐화하는 훌륭한 접근입니다. 이는 ViewModel의 책임을 명확히 분리합니다.
40-41: 상태 플로우 타입 일관성 개선
_tempFilterUiState가LibraryFilterUiModel을 사용하도록 업데이트된 것은 상태 관리의 일관성을 크게 향상시킵니다.
113-123: 도메인 모델 메서드 사용 개선
readStatuses.set(readStatus)와attractivePoints.set(attractivePoint)같은 도메인 모델 메서드 사용은 상태 업데이트 로직을 캡슐화하여 코드 가독성을 향상시킵니다.
139-144: 필터 적용 로직 단순화 성공
selectedKeys와rating.value속성을 통한 필터 적용 로직은 이전보다 훨씬 직관적이고 명확합니다. 도메인 모델의 캡슐화 효과가 잘 드러나는 부분입니다.
58-66: 도메인 모델 변환 로직 정상 확인아래 구현을 모두 확인했으며, 변환 로직에 별다른 이슈가 없습니다.
- ReadStatuses.kt (
List<String>.toReadStatuses()at lines 26–30)- AttractivePoints.kt (
List<String>.toAttractivePoints()at lines 29–33)- SortCriteria.kt (
companion object { fun from(key: String): SortCriteria … }at line 12)- NovelRating.kt (
companion object { fun from(value: Float): NovelRating … }at line 21)추가 수정 없이 이번 PR 머지 가능합니다.
feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryListItem.kt (12)
56-59: 도메인 모델로의 깔끔한 전환UI 레이어에서 도메인 모델을 직접 사용하도록 import가 업데이트되었습니다. 이는 계층 간 명확한 책임 분리를 위한 좋은 아키텍처 개선입니다.
68-68: 타입 안전성 향상
LibraryListItemModel에서NovelUiModel로 변경하여 도메인 중심의 모델 사용으로 전환되었습니다. 이는 UI와 도메인 로직의 결합도를 낮추는 좋은 접근입니다.
121-121: 일관성 있는 모델 사용
ReadStatusUiModel을 명시적으로 사용하여 읽기 상태 표시 로직이 명확해졌습니다.
155-176: 안전한 null 처리 및 도메인 모델 활용
ReadStatusUiModel의 속성들(backgroundColor,readStatus.label)을 직접 활용하여 UI 표현 로직이 개선되었습니다. null 안전성도 적절히 처리되었습니다.
188-188: 함수 시그니처 일관성
NovelUiModel을 매개변수로 받도록 업데이트되어 전체 리팩터링과 일관성을 유지합니다.
203-203: 도메인 모델 활용 개선
NovelUiModel을 직접 사용하여 날짜 정보 표시 로직이 단순화되었습니다.
218-221: 타입 안전성과 의미 명확성 향상
NovelRating도메인 모델 사용으로 평점 데이터의 타입 안전성이 향상되었습니다.AttractivePoints도메인 모델 사용으로 매력 포인트 데이터 구조가 명확해졌습니다.
253-261: 도메인 모델을 통한 안전한 데이터 접근
NovelRating.rating.value를 통해 실제 평점 값에 접근하는 방식으로, 도메인 모델의 캡슐화를 잘 활용하고 있습니다.
243-244: 도메인 모델 구조 활용
AttractivePoints.value.isNotEmpty()를 통해 매력 포인트 존재 여부를 확인하는 로직이 도메인 모델 구조에 맞게 업데이트되었습니다.
309-327: 컬렉션 접근 패턴 개선
AttractivePoints.selectedAttractivePoints를 통해 선택된 매력 포인트들에 직접 접근하는 방식으로 개선되어, 도메인 모델의 의도를 명확히 표현합니다.
330-346: 도메인 엔티티 직접 활용
AttractivePoint도메인 enum을 직접 사용하여label속성에 접근하는 방식으로, UI와 도메인 간의 매핑 로직이 단순화되었습니다.
349-358: enum 기반 아이콘 매핑 개선
AttractivePoint도메인 enum을 직접 사용한 when 표현식으로 아이콘 매핑이 더욱 타입 안전하고 명확해졌습니다. 컴파일 타임에 모든 케이스가 처리되는지 확인할 수 있어 안정성이 향상되었습니다.
|
|
||
| override val jumpingSupported: Boolean get() = targetSource.jumpingSupported | ||
|
|
||
| override fun getRefreshKey(state: PagingState<Key, To>): Key? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
getRefreshKey 구현 개선이 필요합니다.
현재 null을 반환하면 화면 회전 등의 설정 변경 시 페이징이 처음부터 다시 시작됩니다. 원본 PagingSource의 refresh key 로직을 활용해야 합니다.
-override fun getRefreshKey(state: PagingState<Key, To>): Key? = null
+override fun getRefreshKey(state: PagingState<Key, To>): Key? {
+ // PagingState의 타입이 다르므로 anchorPosition 기반으로 처리
+ return state.anchorPosition?.let { position ->
+ state.closestPageToPosition(position)?.prevKey
+ ?: state.closestPageToPosition(position)?.nextKey
+ }
+}🤖 Prompt for AI Agents
In
core/database/src/main/java/com/into/websoso/core/database/datasource/library/paging/MapValuePagingSource.kt
at line 34, the getRefreshKey method currently returns null, causing paging to
restart on configuration changes like screen rotation. Update getRefreshKey to
implement the standard PagingSource refresh key logic by analyzing the
PagingState parameter to return a key that allows the paging to resume from the
last accessed position instead of restarting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 페이징 소스는 단순히 모델을 매핑만해주고 반환하는데 getRefreshKey를 재정의해야돼?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s9hn 맞습니다! 단순 매핑만 하는 래퍼라면 원본 PagingSource에게 위임하는 것이 더 적절합니다.
override fun getRefreshKey(state: PagingState<Key, To>): Key? =
targetSource.getRefreshKey(PagingState(
pages = emptyList(), // 타입이 다르므로 빈 리스트
anchorPosition = state.anchorPosition,
config = state.config,
leadingPlaceholderCount = state.pages.firstOrNull()?.itemsBefore ?: 0
))하지만 PagingState의 제네릭 타입이 다르기 때문에 직접 위임하기 복잡합니다. 실제로는 null 반환도 합리적인 선택일 수 있습니다. 매핑 레이어에서 복잡한 refresh 로직을 구현하는 것보다는 단순하게 처음부터 다시 로드하는 것이 더 안전할 수 있습니다.
현재 구현이 적절해 보입니다. 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai 해당 클래스는 Singleton을 유지하면 안돼? 단순히 매핑만을 위한 클래스인데
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s9hn 정확한 지적입니다! @Singleton 애노테이션은 제거해야 합니다.
문제점:
- PagingSource는 상태를 가짐 - 각 페이징 요청마다 새로운 인스턴스가 필요
- 인스턴스별 의존성 -
targetSource와mapper가 인스턴스마다 다름 - 팩토리 패턴 사용 -
mapValueextension function이 인스턴스를 생성하는 구조
수정 제안:
// @Singleton과 @Inject 제거
internal class MapValuePagingSource<Key : Any, From : Any, To : Any>(
private val targetSource: PagingSource<Key, From>,
private val mapper: suspend (From) -> To,
) : PagingSource<Key, To>() {
// ... 나머지 구현
}단순 매핑 클래스라도 PagingSource의 특성상 각 페이징 컨텍스트마다 새로운 인스턴스가 생성되어야 올바르게 동작합니다.
🐰 좋은 지적이네요! 코드가 더 명확해질 거예요.
✏️ Learnings added
Learnt from: s9hn
PR: Team-WSS/WSS-Android#734
File: core/database/src/main/java/com/into/websoso/core/database/datasource/library/paging/MapValuePagingSource.kt:34-34
Timestamp: 2025-08-01T03:33:19.701Z
Learning: MapValuePagingSource와 같은 PagingSource 래퍼 클래스는 @Singleton을 사용하면 안 된다. PagingSource는 상태를 가지며 각 페이징 요청마다 새로운 인스턴스가 필요하기 때문이다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| @Database( | ||
| entities = [InDatabaseNovelEntity::class, InDatabaseFilteredNovelEntity::class], | ||
| version = 4, | ||
| entities = [InDatabaseNovelEntity::class], | ||
| version = 5, | ||
| exportSchema = false, | ||
| ) | ||
| @TypeConverters(Converters::class) | ||
| internal abstract class WebsosoDatabase : RoomDatabase() { | ||
| internal abstract fun novelDao(): NovelDao | ||
|
|
||
| internal abstract fun filteredNovelDao(): FilteredNovelDao | ||
| } | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| internal object DatabaseModule { | ||
| @Provides | ||
| @Singleton | ||
| internal fun provideDatabase( | ||
| @ApplicationContext context: Context, | ||
| ): WebsosoDatabase = | ||
| Room | ||
| .databaseBuilder( | ||
| context, | ||
| WebsosoDatabase::class.java, | ||
| "websoso.db", | ||
| ).build() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
데이터베이스 스키마 단순화가 잘 되었습니다.
FilteredNovelEntity 제거로 스키마가 단순해졌고, 버전을 5로 증가시킨 것이 적절합니다. 다만 마이그레이션 처리가 필요합니다.
다음 스크립트로 마이그레이션 구현을 확인해주세요:
🏁 Script executed:
#!/bin/bash
# 데이터베이스 마이그레이션 확인
rg -A 10 "Migration.*4.*5" --type kotlin
ast-grep --pattern 'val MIGRATION_4_5 = $_'Length of output: 88
마이그레이션 구현 필요: 버전 4→5 누락 확인
RoomDatabase 버전을 4에서 5로 올리셨지만, 해당 마이그레이션 코드(예: MIGRATION_4_5)와 이를 addMigrations()로 등록하는 부분이 발견되지 않습니다. 스키마 변경 시 데이터 손실을 방지하려면 반드시 마이그레이션을 추가해 주세요.
점검 항목:
MIGRATION_4_5객체(또는 함수) 정의 여부 확인Room.databaseBuilder(...).addMigrations(MIGRATION_4_5)호출 여부 확인
🤖 Prompt for AI Agents
In core/database/src/main/java/com/into/websoso/core/database/WebsosoDatabase.kt
around lines 9 to 17, the migration from version 4 to 5 is missing. Define a
MIGRATION_4_5 object that specifies the schema changes between these versions,
then register this migration by adding addMigrations(MIGRATION_4_5) to the
Room.databaseBuilder call to ensure data integrity during the upgrade.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
•fallbackToDestructiveMigration()은?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@s9hn 좋은 지적입니다! fallbackToDestructiveMigration()을 사용하는 것이 이 경우에는 더 적합할 수 있습니다.
fallbackToDestructiveMigration()은 다음과 같은 상황에서 적절한 선택입니다:
-
캐시된 데이터인 경우:
InDatabaseFilteredNovelEntity는 필터링된 소설 데이터를 캐시하는 용도로 보이며, 이는 다시 가져올 수 있는 데이터입니다. -
PR 목표와 부합: 이번 PR에서 "전체 사용자 서재 데이터를 백그라운드에서 가져오는 무거운 작업을 피하고, 사용자가 명시적으로 요청한 데이터만 캐시"하는 것이 목표라고 하셨는데, 기존 캐시 데이터를 버리고 새로 시작하는 것이 오히려 의도에 맞습니다.
-
데이터 손실이 허용되는 경우: 사용자의 중요한 데이터가 아닌 성능 최적화를 위한 캐시 데이터이므로 손실되어도 문제없습니다.
따라서 DatabaseModule에서 다음과 같이 설정하는 것을 추천합니다:
Room.databaseBuilder(context, WebsosoDatabase::class.java, "websoso.db")
.fallbackToDestructiveMigration()
.build()이렇게 하면 명시적인 마이그레이션 코드 없이도 안전하게 스키마 변경을 처리할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# DatabaseModule에서 fallbackToDestructiveMigration 사용 여부 확인
rg "fallbackToDestructiveMigration" --type kotlin
Length of output: 49
현재 코드베이스에서 fallbackToDestructiveMigration()이 사용되지 않고 있습니다. DatabaseModule의 현재 구현을 확인해서 어떻게 설정되어 있는지 보겠습니다.
🏁 Script executed:
#!/bin/bash
# DatabaseModule 구현 확인
rg -A 10 "Room\.databaseBuilder" --type kotlin
Length of output: 48
🏁 Script executed:
#!/bin/bash
# DatabaseModule 파일 찾기
fd DatabaseModule.kt
# Room 관련 코드 찾기
rg "Room\." --type kotlin -A 3
# 데이터베이스 관련 모듈 찾기
rg "provideDatabase" --type kotlin -A 5
Length of output: 828
🏁 Script executed:
#!/bin/bash
# DatabaseModule 전체 내용 확인
cat core/database/src/main/java/com/into/websoso/core/database/di/DatabaseModule.kt
Length of output: 829
@s9hn 맞습니다! 현재 DatabaseModule을 보니 마이그레이션 처리가 전혀 없는 상태네요. fallbackToDestructiveMigration()을 사용하는 것이 이 상황에서 가장 적절한 해결책입니다.
현재 DatabaseModule을 다음과 같이 수정하시면 됩니다:
@Provides
@Singleton
internal fun provideDatabase(
@ApplicationContext context: Context,
): WebsosoDatabase =
Room
.databaseBuilder(
context,
WebsosoDatabase::class.java,
"websoso.db",
)
.fallbackToDestructiveMigration()
.build()이 방법이 적합한 이유:
-
제거된 엔티티의 성격:
InDatabaseFilteredNovelEntity는 필터링된 소설 데이터를 캐시하는 용도로, 사용자의 핵심 데이터가 아닙니다. -
PR 목표와 일치: "백그라운드에서 모든 사용자 서재 데이터를 가져오는 무거운 작업을 피하고, 사용자가 명시적으로 요청한 데이터만 캐시"한다는 목표에 부합합니다.
-
개발 효율성: 복잡한 마이그레이션 로직을 작성할 필요 없이 깔끔하게 스키마 변경을 처리할 수 있습니다.
-
데이터 특성: 캐시 데이터는 언제든 다시 가져올 수 있으므로 손실되어도 문제없습니다.
명시적 마이그레이션 대신 fallbackToDestructiveMigration()을 사용하는 것이 이번 리팩토링의 의도에 더 적합한 선택인 것 같습니다.
| private suspend fun getUserNovels( | ||
| lastUserNovelId: Long, | ||
| libraryFilter: LibraryFilter, | ||
| ) = runCatching { | ||
| libraryRemoteDataSource.getUserNovels( | ||
| userId = accountRepository.userId, | ||
| lastUserNovelId = lastUserNovelId, | ||
| size = PAGE_SIZE, | ||
| sortCriteria = libraryFilter.sortCriteria, | ||
| isInterest = if (!libraryFilter.isInterested) null else true, | ||
| readStatuses = libraryFilter.readStatuses.ifEmpty { null }, | ||
| attractivePoints = libraryFilter.attractivePoints.ifEmpty { null }, | ||
| novelRating = if (libraryFilter.novelRating.isCloseTo(DEFAULT_NOVEL_RATING)) { | ||
| null | ||
| } else { | ||
| libraryFilter.novelRating | ||
| }, | ||
| query = null, | ||
| updatedSince = null, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
필터 매개변수 매핑이 올바르게 구현되어 있습니다.
getUserNovels 함수가 LibraryFilter의 속성들을 원격 데이터 소스 매개변수로 적절하게 변환하고 있습니다. 특히 기본값 처리(isCloseTo, ifEmpty)가 잘 되어 있습니다.
코드 중복 확인이 필요합니다. UserLibraryRepository.kt에 동일한 구조의 getUserNovels 함수가 있는 것으로 보입니다.
다음 스크립트로 코드 중복을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# 설명: MyLibraryRepository와 UserLibraryRepository에서 getUserNovels 함수의 중복 확인
echo "=== MyLibraryRepository.kt의 getUserNovels 함수 ==="
ast-grep --pattern $'private suspend fun getUserNovels($$$) = runCatching {
$$$
}'
echo -e "\n=== 두 파일에서 libraryRemoteDataSource.getUserNovels 호출 패턴 비교 ==="
rg -A 15 "libraryRemoteDataSource\.getUserNovels" --type kotlinLength of output: 10712
중복된 getUserNovels 구현 리팩터링 필요
UserLibraryRepository.kt와 MyLibraryRepository.kt에 거의 동일한 private suspend fun getUserNovels 구현이 있습니다. userId 매핑만 userId ↔ accountRepository.userId로 차이가 있으므로, 아래 파일들의 중복을 제거하고 공통 로직을 추출해주세요.
- data/library/src/main/java/com/into/websoso/data/library/repository/UserLibraryRepository.kt (라인 50–70)
- data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt (라인 46–66)
대안 예시:
- 공통 베이스 리포지토리 또는 헬퍼 클래스에 로직 이동
- 확장 함수/유틸리티 함수로 분리
🤖 Prompt for AI Agents
In
data/library/src/main/java/com/into/websoso/data/library/repository/MyLibraryRepository.kt
lines 46 to 66 and UserLibraryRepository.kt lines 50 to 70, the private suspend
function getUserNovels is duplicated with only the userId source differing.
Refactor by extracting the common getUserNovels logic into a shared base
repository class or a helper/utility function that accepts userId as a
parameter, then have both repositories call this shared implementation with
their respective userId values to eliminate duplication.
| dependencies { | ||
| implementation(projects.core.common) | ||
| implementation(projects.data.library) | ||
| implementation(libs.paging.runtime) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
도메인 계층이 데이터 계층에 의존하고 있습니다
implementation(projects.data.library) 의존성은 도메인 → 데이터 방향의 의존을 만들며, 클린 아키텍처·레이어드 아키텍처 원칙에 위배됩니다. 도메인 모듈은 하위(공통) 모듈만 참조하고 데이터/인프라스트럭처 모듈에서는 도메인 모듈을 참조하도록 역방향 의존을 끊어주세요.
가능한 대응:
-implementation(projects.data.library)또는 data 모듈에 필요한 도메인 인터페이스를 노출하고, data 모듈에서 해당 인터페이스 구현체를 제공하는 형태로 전환하세요.
🤖 Prompt for AI Agents
In domain/library/build.gradle.kts around lines 11 to 14, the domain module
incorrectly depends on the data module via
implementation(projects.data.library), violating clean architecture principles.
Remove the implementation(projects.data.library) dependency from the domain
module to break this reverse dependency. Instead, expose necessary domain
interfaces from the domain module and have the data module implement these
interfaces, ensuring the data module depends on the domain module, not vice
versa.
| enum class SortCriteria( | ||
| val key: String, | ||
| val label: String, | ||
| ) { | ||
| RECENT("RECENT", "최신 순"), | ||
| OLD("OLD", "오래된 순"), | ||
| ; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
key 필드가 사실상 중복값이라면 제거하거나 의미를 분리하세요.
key 값이 enum name과 동일(“RECENT”, “OLD”)하여 중복 정보만 유지되고 있습니다.
의도적으로 나중에 다른 문자열을 사용하려는 계획이 없다면 key 필드를 삭제해 간결성을 확보하거나, 반대로 실제로 다른 값을 노출하려면 아래 companion object 구현을 수정해 it.key == key를 사용하도록 분리해야 합니다.
- companion object {
- fun from(key: String): SortCriteria = entries.find { it.name == key } ?: RECENT
- }
+ companion object {
+ fun from(key: String): SortCriteria = entries.find { it.key == key } ?: RECENT
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
domain/library/src/main/java/com/into/websoso/domain/library/model/SortCriteria.kt
around lines 3 to 9, the 'key' field duplicates the enum name values and adds no
unique information. To fix this, either remove the 'key' field entirely to
simplify the enum or, if different string values are intended for 'key', update
any companion object methods to compare using 'it.key == key' instead of the
enum name, ensuring the 'key' field serves a distinct purpose.
📌𝘐𝘴𝘴𝘶𝘦𝘴
📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯
💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴
잦은 수정과 QA, 복잡한 필터 상태값들로 계층 간 상태 중복과 혼동이 많아 이번 이슈에서 한번 정리했습니다!
전체 서재 DB 제거
현재 전체 서재 DB를 사용하기 위해선 워크매니저 등을 이용해 백그라운드에서 각 유저별 모든 서재 데이터를 패칭해야합니다.
본 작업의 작업소요량이 매우 클것으로 예상되어 이번 스프린트에선 진행하지 않겠습니다.
그렇지만 여전히 사용자가 '호출한 데이터'만은 캐싱되므로, 여전히 네트워크 통신 빈도수는 크지 않습니다!
데이터레이어에서 데이터베이스 의존성 분리
최상위 계층인 데이터레이어에서 페이징을 사용하기 위해 어쩔 수 없이 DB모듈을 의존해야했습니다.
PagingSource의 반환타입을 강제로 변경하여 DB 엔티티는 DB 모듈에만 캡슐화할 수 있도록 했습니다.
결과적으로, 데이터레이어는 누구에게도 의존하지 않는 최상위 계층을 유지하며, 각 모듈은 역할과 책임에 맞게 캡슐화 유지했습니다.
상태값 정리
UI state, 도메인 모델, 데이터 모델, DB 모델 등 여러 레이어에 두루 쓰이는 '필터 상태값'이 너무 혼재되어 있었습니다.
도메인 레이어 이전은 Map, 이후는 List로 명확히 구분하여 사용할 수 있도록 합니다.
도메인 레이어는 각 상태 모델들을 캡슐화하며, 계층간 원활한 형변환 및 API를 지원합니다.
일급 컬렉션과 값객체를 만들었으나, 상태를 객체 내부에서 관리하게 되면 UI에서 수집할 수 없으므로 data class로 변경 및 구현했습니다.
Summary by CodeRabbit
신규 기능
NovelUiModel) 기반으로 일관성 있게 개선되었습니다.기능 개선 및 리팩터링
버그 수정
불필요 코드/모델 제거
의존성 및 설정
이 업데이트로 라이브러리 필터링과 소설 탐색 경험이 더욱 직관적이고 강력해집니다.