#178 feat: 개설학과 firebase 적용#179
Conversation
- 0f80dbf commit을 develop branch에 맞게 적용
Walkthrough이 변경 사항은 시간표(오픈 강의) 데이터의 관리 방식을 기존 REST API에서 파이어베이스 Realtime Database로 전환하고, 이를 지원하기 위한 로컬 캐싱, 데이터 변환, 의존성 주입, UI 및 도메인 계층의 리팩토링을 포함합니다. 또한 오픈 강의 DB의 Room 스키마와 관련 유틸리티, 테스트, 의존성 설정이 추가 및 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI
participant ViewModel
participant UseCase
participant Repository
participant LocalDataSource
participant RemoteDataSource
participant Firebase
UI->>ViewModel: initData()
ViewModel->>UseCase: UpdateOpenLectureIfNeedUseCase()
UseCase->>Repository: checkNeedUpdate()
Repository->>LocalDataSource: getOpenLectureListVersion()
Repository->>RemoteDataSource: getOpenLectureListVersion()
RemoteDataSource->>Firebase: fetch version
Repository-->>UseCase: Boolean(업데이트 필요 여부)
alt 업데이트 필요
UseCase->>Repository: updateAllLectures()
Repository->>RemoteDataSource: getOpenLectureList()
RemoteDataSource->>Firebase: fetch lecture list
Repository->>LocalDataSource: updateAllLectures()
Repository->>LocalDataSource: setOpenLectureListVersion()
end
ViewModel->>Repository: getLastUpdatedDate()
Repository->>LocalDataSource: getOpenLectureListVersion()
ViewModel->>Repository: getOpenLectureList()
Repository->>LocalDataSource: getOpenLectureList()
LocalDataSource-->>Repository: Flow<List<OpenLecture>>
Repository-->>ViewModel: Flow<List<OpenLecture>>
ViewModel-->>UI: 상태 갱신 및 리스트 표시
Assessment against linked issues
Poem
Tip ⚡️ Faster reviews with caching
Enjoy the performance boost—your workflow just got faster. ✨ Finishing Touches
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 (
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (20)
remote/common/build.gradle.kts (1)
33-35: Firebase BOM 및 의존성 추가 검토 요청
libs.firebase.bom을 플랫폼으로 설정하고libs.bundles.firebase를 통해 Firebase 종속성을 일관된 버전으로 관리하도록 했습니다.
libs.bundles.firebase에firebase-database-ktx등 실제 사용하는 모듈이 포함되었는지 확인해주세요.- 불필요한 모듈을 최소화하려면 해당 번들 대신 필요한 개별 라이브러리만 추가하는 방안을 고려할 수 있습니다.
remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt (1)
14-18: 오프라인 캐시 활성화 옵션 고려
FirebaseDatabase.getInstance()호출 직후firebaseDatabase.setPersistenceEnabled(true)를 호출해 네트워크 연결이 불안정할 때도 로컬 캐싱된 데이터를 활용할 수 있도록 개선을 권장합니다.remote/timetable/build.gradle.kts (1)
17-19: Firebase 종속성 최적화 제안
Firebase BOM과 번들(libs.bundles.firebase)로 버전 관리를 통일한 점은 좋습니다. 다만, 빌드 크기 최적화를 위해 실제 사용하는firebase-database-ktx모듈만 명시적으로 추가하는 방안을 함께 고려해보세요.presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt (1)
122-124: Firebase 데이터 통합 로직 구현 확인기존 API와 Firebase에서 가져온 개설학과 목록을 통합하고 중복을 제거하는 로직이 적절하게 구현되었습니다.
distinct()를 사용하여 중복 방지를 처리한 점이 좋습니다.다만, Firebase 데이터 가져오기가 실패할 경우에 대한 예외 처리가 없습니다. 오류 발생 시 기존 API 데이터만이라도 표시될 수 있도록 예외 처리를 고려해보세요.
다음과 같이 예외 처리를 추가하는 것을 권장합니다:
- val firebaseOpenMajor = openLectureRepository.getOpenMajor() - allOpenMajorList.addAll((it + firebaseOpenMajor).distinct()) + val firebaseOpenMajor = try { + openLectureRepository.getOpenMajor() + } catch (e: Exception) { + postSideEffect(OpenMajorSideEffect.HandleException(e)) + emptyList() + } + allOpenMajorList.addAll((it + firebaseOpenMajor).distinct())presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt (1)
218-225: Firebase에서 가져온 데이터의 최근 갱신일 표시 추가.Firebase에서 가져온 개설학과 데이터의 최근 갱신일을 사용자에게 표시하는 UI 요소가 적절히 추가되었습니다. 사용자에게 데이터의 최신성을 알려주는 좋은 UX 개선입니다.
날짜 형식을 더 사용자 친화적으로 표시할 수도 있습니다:
- text = "최근 갱신일: ${uiState.lastUpdatedDate ?: "확인 중"}", + text = "최근 갱신일: ${uiState.lastUpdatedDate?.let { formatDate(it) } ?: "확인 중"}",ViewModel이나 유틸리티 클래스에 formatDate 함수를 추가하여 날짜 형식을 일관되게 관리할 수 있습니다.
domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt (1)
1-14: 클린 아키텍처 패턴을 잘 따르고 있는 UseCase 구현입니다.이 UseCase는 개설학과 데이터 업데이트가 필요한지 확인하고 필요한 경우 업데이트를 수행하는 기능을 담당합니다.
runCatchingIgnoreCancelled를 사용하여 CancellationException은 다시 던지고 다른 예외는 Result로 래핑하는 패턴이 적절하게 적용되었습니다.한 가지 제안하자면,
if(openLectureRepository.checkNeedUpdate().not()) return@runCatchingIgnoreCancelled구문에 공백을 추가하여if (openLectureRepository.checkNeedUpdate().not()) return@runCatchingIgnoreCancelled와 같이 작성하면 가독성이 더 좋아질 것입니다.local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt (1)
51-59: Room 데이터베이스 제공 메서드가 잘 구현되었습니다.OpenLectureDatabase를 제공하는 메서드가 표준 Room.databaseBuilder 패턴을 따라 올바르게 구현되었습니다. 하지만 다른 데이터베이스와 달리
fallbackToDestructiveMigration()이 없습니다.아래와 같이 마이그레이션 폴백 처리를 추가하는 것이 좋을 것 같습니다:
@Provides @Singleton fun provideOpenLectureDatabase(@ApplicationContext context: Context): OpenLectureDatabase { return Room.databaseBuilder( context, OpenLectureDatabase::class.java, DatabaseName.OPEN_LECTURE - ).build() + ).fallbackToDestructiveMigration() + .build() }data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt (1)
3-11: Firebase에서 가져올 개설학과 데이터를 위한 모델이 잘 구현되었습니다.Firebase Realtime Database에서 가져올 개설학과 데이터를 위한 Raw 모델 클래스가 적절히 정의되었습니다. 각 필드에 대한 한글 주석이 포함되어 이해하기 쉽습니다.
다음 사항을 고려해보세요:
- 객체 직렬화를 위한
@Serializable어노테이션 추가- 필드에 nullable 타입 또는 기본값 지정 전략을 일관되게 적용
+import kotlinx.serialization.Serializable +@Serializable data class OpenLectureRaw( val number: Long = 0, // 번호 val major: String = "", // 개설학과 val grade: Int = 1, // 개설학년 val className: String = "", // 과목명 val classification: String = "", // 이수 구분 val professor: String = "", val time: String = "", )local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt (1)
8-20: Room 데이터베이스 타입 변환기가 올바르게 구현되었습니다.CellEntity 리스트와 JSON 문자열 간의 변환을 위한 Room TypeConverter가 잘 구현되었습니다.
ignoreUnknownKeys = true설정은 향후 모델 변경에 대한 하위 호환성을 제공합니다.예외 처리를 추가하는 것이 좋을 것 같습니다:
@TypeConverter fun toCellList(value: String): List<CellEntity> { + return try { return json.decodeFromString(value) + } catch (e: Exception) { + emptyList() + } }schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json (1)
1-70: Room 데이터베이스 스키마가 적절히 정의되었습니다.개설학과 데이터를 위한 Room 데이터베이스 스키마가 잘 정의되었습니다. 기본 키 자동 생성과 필요한 모든 필드가 포함되어 있습니다.
다만, 성능 최적화를 위해 다음을 고려해보세요:
- 자주 검색하는 열에 인덱스 추가 (예: professorName, major)
cellList가 TEXT로 저장되므로 데이터 크기에 따라 쿼리 성능에 영향이 있을 수 있습니다.{ "formatVersion": 1, "database": { "version": 1, "identityHash": "a9fed779395cf5534baf92fbef19edd1", "entities": [ { "tableName": "OpenLectureEntity", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `major` TEXT NOT NULL, `grade` INTEGER NOT NULL, `professorName` TEXT NOT NULL, `cellList` TEXT NOT NULL)", "fields": [ ... ], "primaryKey": { "autoGenerate": true, "columnNames": [ "id" ] }, "indices": [], + "indices": [ + { + "name": "index_OpenLectureEntity_major", + "unique": false, + "columnNames": ["major"], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_OpenLectureEntity_major` ON `${TABLE_NAME}` (`major`)" + }, + { + "name": "index_OpenLectureEntity_professorName", + "unique": false, + "columnNames": ["professorName"], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_OpenLectureEntity_professorName` ON `${TABLE_NAME}` (`professorName`)" + } + ], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a9fed779395cf5534baf92fbef19edd1')" ] } }local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt (1)
14-26: 검색 기능이 잘 구현되었습니다.LIKE 연산자를 사용하여 부분 일치 검색을 구현한 방식이 좋습니다. NULL 파라미터 처리도 올바르게 되어 있습니다.
다만, 고려할 점:
- 대량의 데이터를 검색할 때 성능 최적화가 필요할 수 있습니다(스키마에 인덱스 추가).
- 대소문자 구분을 하는지 확인이 필요합니다. SQLite에서는 기본적으로 대소문자를 구분하지 않지만, 일관성을 위해 명시적으로 처리할 수 있습니다.
data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt (1)
7-41: 시간표 문자열 파싱 로직이 효과적으로 구현되었습니다.정규식을 활용한 시간표 문자열 파싱 로직이 체계적으로 구현되었으나, 빈 입력 처리가 미흡합니다.
빈 문자열이나 잘못된 형식의 입력에 대한 추가 유효성 검사가 필요합니다:
fun parseTimeTableString(input: String): List<Cell> { + if (input.isBlank()) return emptyList() + val cellRegex = """([^(]+)\(([^)]+)\)""".toRegex() val dayPeriodRegex = """([월화수목금토])([\d,]+)""".toRegex() // 기존 코드...local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt (2)
27-30: Preference 키 이름을 의미 있는 상수로 교체하세요
"[KEY] is open lecture list version"문자열은 띄어쓰기와 불필요한 서술형 표현으로 구성되어 있어 가독성과 일관성이 떨어집니다. 다른 DataStore 키와도 형식이 맞지 않을 가능성이 높습니다. 명확하고 상수형 이름으로 변경하면 유지보수성이 좋아집니다.- private val OPEN_LECTURE_LIST_VERSION = longPreferencesKey("[KEY] is open lecture list version") + private val KEY_OPEN_LECTURE_LIST_VERSION = longPreferencesKey("OPEN_LECTURE_LIST_VERSION")
57-61: 데이터베이스 일관성을 위한 트랜잭션 보장 필요
updateAllLectures에서는 전체 레코드를 갱신하지만, DAO 측에서@Transaction어노테이션으로 래핑돼 있지 않다면 중간 실패 시 일부만 반영될 위험이 있습니다. DAO 메서드가 트랜잭션을 보장하도록 확인하거나, 여기서openLectureDatabase.runInTransaction { … }로 감싸는 것을 권고드립니다.data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt (2)
65-79: null 버전 처리 로직 개선
firstOrNull()결과가null이면"null"문자열로 변환돼length != 12검사까지 진행됩니다. 명시적 null 체크가 더 명확하며, 불필요한 문자열 변환을 줄일 수 있습니다.- val version = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull().toString() - if (version.length != 12) { + val versionLong = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull() + ?: return null + val version = versionLong.toString() + if (version.length != 12) { return null }
82-84: 학과 목록 중복 제거 누락
getOpenMajor()는 중복 학과명을 그대로 반환합니다. UI 드롭다운 등에 바로 사용할 경우 중복 항목이 표시됩니다.distinct()호출로 간단히 해결 가능합니다.- return localOpenLectureDataSource.getOpenLectureList().map { list -> list.map { it.major } }.firstOrNull() ?: emptyList() + return localOpenLectureDataSource.getOpenLectureList() + .map { list -> list.map { it.major }.distinct() } + .firstOrNull() ?: emptyList()domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt (1)
17-20: 학과 목록 메서드 설명 명확화 필요
getOpenMajor()가 중복 제거 여부, 정렬 여부를 명시하지 않아 구현체마다 결과가 달라질 수 있습니다. KDoc 주석으로 계약(contract)을 명확히 정의하면 도메인 계층의 일관성이 확보됩니다.presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt (2)
35-39: 의존성 주입 방향은 👍, 그러나OpenLectureRepository까지 뷰모델에서 직접 주입하는 것이 계층 간 책임을 흐릴 수 있습니다.
lastUpdatedDate조회만을 위해 Repository 를 노출시키면 ViewModel 에서 Data-Layer 세부 구현이 새어 나옵니다.
UpdateOpenLectureIfNeedUseCase와 같은 패턴으로 전용 UseCase (GetLastUpdatedDateUseCase) 를 만드는 편이 Domain 계층 경계를 지키고, 테스트/모킹도 수월합니다.
172-173: 하드코딩된 “전체” 문자열은 상수/Enum 으로 추출하세요다국어 지원이나 문자열 변경 시 ViewModel 수정이 필요합니다.
object OpenMajor { const val ALL = "전체" }또는 Enum 으로 관리하면 안전합니다.local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt (1)
12-12: Import 경로 업데이트 및 패키지 구조 검토
TimetableDatabase가 새로운 위치(com.suwiki.local.common.database.database)로 옮겨진 것이 반영되었습니다.- 다만
database.database로 패키지명이 중복되어 보여, 네이밍 일관성을 위해 불필요한 중간 패키지 레벨을 제거하는 방안을 고려해볼 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (42)
data/timetable/build.gradle.kts(1 hunks)data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt(1 hunks)data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt(1 hunks)data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt(1 hunks)data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt(1 hunks)data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt(1 hunks)domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt(1 hunks)domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt(1 hunks)domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt(1 hunks)gradle/libs.versions.toml(2 hunks)local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/database/TimetableDatabase.kt(1 hunks)local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt(2 hunks)local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt(2 hunks)local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt(1 hunks)local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt(1 hunks)local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt(1 hunks)local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt(1 hunks)local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt(1 hunks)local/timetable/src/main/java/com.suwiki.local.timetable/di/LocalDataSourceModule.kt(2 hunks)presentation/openmajor/build.gradle.kts(1 hunks)presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt(3 hunks)presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt(1 hunks)presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt(1 hunks)presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt(6 hunks)remote/common/build.gradle.kts(2 hunks)remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt(1 hunks)remote/timetable/build.gradle.kts(1 hunks)remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt(1 hunks)schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json(1 hunks)schemas/com.suwiki.local.common.database.OpenLectureDatabase/2.json(1 hunks)schemas/com.suwiki.local.common.database.TimetableDatabase/1.json(2 hunks)schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json(1 hunks)schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json(1 hunks)schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json(1 hunks)schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json(1 hunks)schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json(1 hunks)schemas/com.suwiki.local.common.database.database.TimetableDatabase/2.json(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt (1)
domain/common/src/main/java/com/suwiki/domain/common/RunCatchingIgnoreCancelled.kt (1)
runCatchingIgnoreCancelled(5-8)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (40)
remote/common/build.gradle.kts (1)
13-13: 디버그용 BASE_URL 도메인 주소로 업데이트 확인
디버그 빌드 타입의BASE_URL을 내부 IP 대신 프로덕션 도메인(https://api.suwiki.kr)으로 변경하여 개발 편의성 및 일관성을 확보했습니다.remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt (1)
1-12: 패키지 선언 및 Hilt 모듈 설정 부분은 표준 구조로, 별도 의견 없습니다.schemas/com.suwiki.local.common.database.database.TimetableDatabase/2.json (1)
58-58: 단순 개행 추가로, 별도 검토 불필요합니다.data/timetable/build.gradle.kts (1)
12-12: JUnit4 테스트 의존성 추가 승인
data/timetable모듈에testImplementation(libs.junit4)를 추가하여 단위 테스트 환경을 확장했습니다.local/common/src/main/java/com/suwiki/local/common/database/database/TimetableDatabase.kt (1)
1-1: 패키지 경로 업데이트 확인
TimetableDatabase의 패키지가com.suwiki.local.common.database.database로 변경되었습니다. 다른 모듈(DAO, DI 설정 등)에서도 동일하게 반영되었는지 검증해주세요.local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt (1)
1-1: 패키지 경로 업데이트 승인
OpenMajorDatabase의 패키지가com.suwiki.local.common.database.database로 이동되었습니다. 전체 프로젝트에서 import 경로가 일관되게 변경되었음을 확인했습니다.local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt (1)
11-11: import 경로 업데이트 승인
OpenMajorDatabase의 import가com.suwiki.local.common.database.database.OpenMajorDatabase로 올바르게 갱신되었습니다. 로컬 데이터 소스 참조에 문제가 없습니다.local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt (1)
6-6: import 경로 업데이트 승인
테스트 코드에서TimetableDatabaseimport가com.suwiki.local.common.database.database.TimetableDatabase로 정확히 변경되었습니다.presentation/openmajor/build.gradle.kts (1)
12-12: 도메인 의존성 추가 확인새로운 timetable 도메인 의존성이 올바르게 추가되었습니다. 이것은 Firebase를 통한 개설학과 데이터 처리를 위한 중요한 변경사항입니다.
presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt (1)
23-23: 상태에 업데이트 날짜 필드 추가 완료Firebase에서 가져온a 개설학과 데이터의 마지막 업데이트 날짜를 표시하기 위한 필드가 적절하게 추가되었습니다. 사용자에게 데이터 신선도를 알려주는 좋은 개선사항입니다.
gradle/libs.versions.toml (2)
182-182: Firebase Database 의존성 추가 확인Firebase Realtime Database KTX 의존성이 올바르게 추가되었습니다. 이는 개설학과 데이터를 Firebase에서 가져오기 위한 필수 라이브러리입니다.
208-208: Firebase 번들 업데이트 확인Firebase 번들에 Database 의존성을 포함시킨 것이 확인되었습니다. 이를 통해 프로젝트의 다른 모듈에서 bundle 방식으로 firebase 의존성을 사용하는 경우에도 Database 기능을 사용할 수 있게 되었습니다.
presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt (2)
11-11: OpenLectureRepository 임포트 확인Timetable 도메인의 Repository를 임포트하여 Firebase 데이터 소스를 사용할 준비가 되었습니다.
37-37: 의존성 주입 확인OpenLectureRepository가 ViewModel 생성자에 올바르게 주입되었습니다. 이로써 Firebase에서 학과 데이터를 가져올 수 있게 되었습니다.
presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt (1)
229-229: 빈 화면 표시 조건 개선.로딩 중일 때는 빈 화면 UI를 표시하지 않도록 로직이 개선되었습니다. 이는 사용자 경험을 향상시키는 좋은 변경사항입니다.
schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json (1)
1-40:Details
✅ Verification successful
Firebase 통합을 위한 로컬 데이터베이스 스키마 추가.
Firebase에서 가져온 개설학과 데이터를 로컬에 캐싱하기 위한 Room 데이터베이스 스키마가 적절히 정의되었습니다. 이는 오프라인 지원과 데이터 로딩 성능 향상에 도움이 됩니다.
스키마 버전이 2로 설정되어 있는데, 버전 1에서 어떤 변경이 있었는지 확인해 보세요. 마이그레이션 로직이 필요한지 검토가 필요할 수 있습니다.
🏁 Script executed:
#!/bin/bash # OpenMajorDatabase 버전 1 스키마가 존재하는지 확인 fd -t f "1.json" --full-path "schemas/com.suwiki.local.common.database.database.OpenMajorDatabase" # 마이그레이션 관련 코드가 존재하는지 확인 rg -A 5 "Migration.*OpenMajorDatabase" --type kotlinLength of output: 987
마이그레이션 로직 확인 완료
Firebase 통합을 위한 로컬 데이터베이스 스키마가 잘 정의되었으며, 오프라인 지원 및 성능 향상을 위해 적절합니다. 추가로 다음 사항을 확인해주세요:
local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt에서
@AutoMigration(from = 1, to = 2, spec = OpenMajorDatabase.RenameTableAutoMigration::class)로 버전 1→2 마이그레이션이 이미 설정되어 있습니다.OpenMajorDatabase.RenameTableAutoMigration스펙 클래스에서 테이블 이름 변경 로직이 올바르게 처리되는지 검토하세요.위 확인 후에는 추가 작업 없이 코드 변경을 승인합니다.
local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt (1)
24-25: OpenLectureDao 의존성 주입 설정 추가.Firebase에서 가져온 개설강의 데이터를 로컬에 저장하기 위한 DAO 의존성 주입이 적절히 구현되었습니다. 기존 패턴과 일관성 있게 작성되었습니다.
local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt (1)
1-15: 적절한 Room 데이터베이스 설정입니다.Room 데이터베이스 구현이 잘 되어 있으며,
OpenLectureEntity를 포함하고OpenLectureConverter를 타입 변환기로 사용하는 설정이 명확합니다. 데이터베이스 버전 1로 시작하며 DAO 접근 방법도 올바르게 구현되어 있습니다.파이어베이스와의 통합을 위한 로컬 캐싱 레이어로 적절히 설계되었습니다.
schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json (1)
1-40: 개설학과 데이터를 위한 적절한 스키마 정의입니다.개설학과(OpenMajor) 데이터를 위한 Room 데이터베이스 스키마가 잘 정의되어 있습니다.
name(TEXT)과id(INTEGER, 프라이머리 키, 자동 증가)의 간단한 구조로 학과 정보를 저장하기에 적합합니다.파이어베이스에서 가져온 학과 데이터를 로컬에 캐싱하는 데 효과적인 구조입니다.
schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json (1)
1-58: 시간표 데이터를 위한 적절한 데이터베이스 스키마입니다.시간표(Timetable) 정보를 저장하기 위한 스키마가 잘 구성되어 있습니다.
createTime을 프라이머리 키로 사용하고, 연도, 학기, 시간표 이름 및 JSON 형태의 시간표 데이터를 저장하는 구조가 적절합니다.
timeTableJsonData필드를 사용하여 시간표 데이터의 복잡한 구조를 JSON으로 직렬화하는 방식은 데이터 구조 변경에 유연하게 대응할 수 있는 장점이 있습니다. 다만, 특정 필드 기반 쿼리가 필요한 경우에는 별도의 처리가 필요할 수 있음을 염두에 두세요.local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt (2)
5-7: 새로운 데이터베이스 관련 임포트가 올바르게 추가되었습니다.Firebase에서 개설학과 데이터를 가져오기 위해 필요한 데이터베이스 클래스들이 잘 임포트되었습니다.
65-65: 데이터베이스 이름 상수가 적절히 추가되었습니다.Firebase 연동을 위한 OpenLecture 데이터베이스 이름 상수가 일관된 형식으로 잘 정의되었습니다.
local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt (2)
28-29: 충돌 전략이 적절히 설정되었습니다.
OnConflictStrategy.REPLACE를 사용하여 충돌 시 기존 데이터를 대체하는 전략이 적절합니다.
34-38: 트랜잭션 처리가 잘 되어 있습니다.트랜잭션을 사용하여 모든 레코드를 원자적으로 업데이트하는 방식이 좋습니다. 데이터 일관성을 보장하는 중요한 패턴입니다.
local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt (3)
8-16: 엔티티-모델 변환 함수가 잘 구현되었습니다.엔티티에서 도메인 모델로의 변환이 적절하게 구현되었습니다.
cellList컬렉션에 대한 매핑도 잘 처리되었습니다.
18-26: 모델-엔티티 변환 함수가 잘 구현되었습니다.도메인 모델에서 엔티티로의 변환이 적절하게 구현되었습니다. 양방향 변환이 일관되게 처리되고 있습니다.
28-40: Cell 및 CellEntity 변환 함수가 잘 구현되었습니다.Cell과 CellEntity 간의 양방향 변환 함수가 간결하고 명확하게 구현되었습니다. 모든 필드가 적절히 매핑되었습니다.
local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt (1)
9-18: OpenLectureEntity 정의가 적절합니다.Room 데이터베이스 엔티티가 잘 정의되었습니다. 자동 생성 ID와 필요한 모든 필드가 포함되어 있습니다.
cellList에 대한 기본값 설정이 좋습니다.schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json (1)
1-88: Room 데이터베이스 스키마가 적절하게 정의되었습니다.Firebase에서 가져온 개설학과 데이터를 로컬에 캐싱하기 위한 데이터베이스 스키마가 잘 구성되어 있습니다. 각 필드의 타입과 제약조건이 적절히 설정되어 있으며, 기본키로
number필드를 사용하는 것이 좋은 접근입니다.한 가지 고려할 점은 자주 검색될 수 있는
major,professor,className등의 필드에 인덱스를 추가하면 쿼리 성능이 향상될 수 있습니다. 현재는 인덱스가 정의되어 있지 않습니다.schemas/com.suwiki.local.common.database.TimetableDatabase/1.json (1)
5-85:Details
✅ Verification successful
시간표 데이터베이스 스키마가 개설학과 스키마와 일관되게 변경되었습니다.
기존의
TimeTableList테이블이TimetableEntity로 변경되었고, 필드 구조가OpenLectureEntity와 일치하도록 재정의되었습니다. 이는 두 데이터 모델 간의 일관성을 유지하고 데이터 변환을 단순화하는 좋은 접근법입니다.데이터베이스 버전이 그대로 1로 유지되고 있는데, 스키마 변경이 있었으므로 마이그레이션 코드가 있는지 확인해야 합니다. 기존 데이터가 손실되지 않도록 적절한 마이그레이션 전략이 필요합니다.
🏁 Script executed:
#!/bin/bash # 데이터베이스 마이그레이션 관련 코드 확인 fd "Migration" --type f --extension ktLength of output: 130
🏁 Script executed:
#!/bin/bash sed -n '1,200p' local/common/src/main/java/com/suwiki/local/common/database/migration/TimetableMigration.ktLength of output: 3774
TimetableDatabase.kt에서 @database 어노테이션의 version과 마이그레이션 등록 여부를 확인하려면 다음 스크립트를 실행해주세요.
🏁 Script executed:
#!/bin/bash file=$(fd TimetableDatabase.kt -e kt | head -n1) sed -n '1,200p' "$file"Length of output: 827
마이그레이션 코드 및 버전 업데이트 확인됨
TimetableDatabase.kt의@Database버전이 2로 올바르게 업데이트되었습니다.TimetableMigration.kt에Migration(1, 2)구현(TIMETABLE_MIGRATION_1_2)이 존재함이 확인되었습니다.위로 기존 스키마 변경에 대응한 마이그레이션 전략이 마련되었으므로 추가 작업 없이 코드 변경을 승인합니다.
data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt (1)
1-18: Firebase 데이터 동기화를 위한 로컬 데이터소스 인터페이스가 잘 설계되었습니다.개설학과 데이터를 Firebase에서 가져와 로컬에 캐싱하기 위한 인터페이스가 적절하게 정의되었습니다. 특히 다음과 같은 장점이 있습니다:
- Flow를 사용하여 반응형 데이터 스트림을 제공하므로 UI가 데이터 변경에 자동으로 반응할 수 있습니다.
- 버전 관리 기능이 포함되어 서버와 로컬 데이터의 동기화 상태를 확인할 수 있습니다.
- 필터링 옵션을 통해 유연한 데이터 쿼리가 가능합니다.
데이터의 부분 업데이트 기능이 없는 것처럼 보이므로, 대용량 데이터를 다룰 경우
updateAllLectures메서드의 성능을 고려할 필요가 있습니다.domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt (2)
11-18: Flow 반환 형식으로의 전환이 잘 이루어졌습니다.Firebase 적용으로 API 호출이 아닌 로컬 캐싱 데이터를 Flow로 반환하는 방식으로 변경되어 데이터 스트림 관찰이 가능해졌습니다. 이는 UI 레이어에서 데이터 변경을 실시간으로 반영하기에 적합합니다.
22-22: 파라미터를 Nullable로 변경한 것은 좋은 선택입니다.lectureOrProfessorName을 nullable로 변경함으로써 검색어 없이도 모든 개설 강의를 조회할 수 있게 되었습니다. 이는 사용자 경험 향상에 도움이 됩니다.
remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt (4)
3-7: Firebase 관련 의존성이 적절히 추가되었습니다.Firebase Database와 Kotlin 코루틴 확장 함수를 적절히 사용하여 비동기 처리를 구현했습니다.
11-11: Firebase Database 의존성 주입이 잘 구현되었습니다.DI 패턴을 통해 FirebaseDatabase 인스턴스를 주입받아 테스트 용이성을 확보했습니다.
14-21: 버전 관리 로직이 적절히 구현되었습니다.Firebase에서 버전 정보를 가져와 long 타입으로 변환하는 로직이 잘 구현되었습니다. 기본값 0 설정도 적절합니다.
43-55: 상수 사용이 깔끔하게 구현되었습니다.필드명과 데이터베이스 경로를 상수로 정의하여 코드 가독성과 유지보수성을 높였습니다.
data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt (1)
43-57: 연속된 정수 그룹화 로직이 효율적으로 구현되었습니다.연속된 시간대를 효과적으로 그룹화하는 확장 함수가 잘 구현되었습니다. 빈 리스트 처리도 적절합니다.
data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt (2)
3-3: 적절한 의존성이 추가되었습니다.Firebase 데이터 매핑을 위한 OpenLectureRaw 모델 의존성이 추가되었습니다.
6-7: 인터페이스 설계가 깔끔하게 개선되었습니다.버전 확인과 데이터 조회 기능이 분리되어 역할이 명확해졌습니다. 이는 단일 책임 원칙(SRP)에 부합합니다.
| @Singleton | ||
| @Binds | ||
| abstract fun bindLocalOpenLectureDataSource( | ||
| localOpenLectureDatasourceImpl: LocalOpenLectureDatasourceImpl, | ||
| ): LocalOpenLectureDataSource |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
LocalOpenLectureDataSource 바인딩 추가.
Firebase에서 가져온 개설강의 데이터를 로컬에서 관리하기 위한 데이터 소스 바인딩이 적절히 구현되었습니다. 기존 패턴과 일관성 있게 작성되었습니다.
구현체인 LocalOpenLectureDatasourceImpl가 올바르게 구현되었는지 확인해 보세요:
🏁 Script executed:
#!/bin/bash
# LocalOpenLectureDatasourceImpl 구현체 코드 확인
rg -A 10 "class LocalOpenLectureDatasourceImpl" --type kotlin
# 관련 테스트 코드가 있는지 확인
fd -t f "LocalOpenLectureDataSource.*Test" --full-path .Length of output: 1706
LocalOpenLectureDataSource 구현체 및 바인딩 확인
LocalDataSourceModule.kt의 바인딩이 기존 패턴과 일치하게 잘 작성되었습니다.LocalOpenLectureDatasourceImpl.kt구현체도DataStore,CoroutineDispatcher,OpenLectureDatabase의존성을 주입받아 올바르게 구성되어 있습니다.- 다만, 해당 데이터 소스에 대한 단위/통합 테스트가 존재하지 않습니다. 동작 보증을 위해 관련 테스트를 추가해 주세요.
| { | ||
| "formatVersion": 1, | ||
| "database": { | ||
| "version": 1, | ||
| "identityHash": "d388602654d094c34e2c718a802d0243", | ||
| "entities": [ | ||
| { | ||
| "tableName": "OpenLectureEntity", | ||
| "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`number` INTEGER NOT NULL, `major` TEXT NOT NULL, `grade` TEXT NOT NULL, `classNumber` TEXT NOT NULL, `classDivideNumber` TEXT NOT NULL, `className` TEXT NOT NULL, `classification` TEXT NOT NULL, `professor` TEXT NOT NULL, `time` TEXT NOT NULL, `credit` TEXT NOT NULL, PRIMARY KEY(`number`))", | ||
| "fields": [ | ||
| { | ||
| "fieldPath": "number", | ||
| "columnName": "number", | ||
| "affinity": "INTEGER", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "major", | ||
| "columnName": "major", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "grade", | ||
| "columnName": "grade", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classNumber", | ||
| "columnName": "classNumber", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classDivideNumber", | ||
| "columnName": "classDivideNumber", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "className", | ||
| "columnName": "className", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classification", | ||
| "columnName": "classification", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "professor", | ||
| "columnName": "professor", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "time", | ||
| "columnName": "time", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "credit", | ||
| "columnName": "credit", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| } | ||
| ], | ||
| "primaryKey": { | ||
| "autoGenerate": false, | ||
| "columnNames": [ | ||
| "number" | ||
| ] | ||
| }, | ||
| "indices": [], | ||
| "foreignKeys": [] | ||
| } | ||
| ], | ||
| "views": [], | ||
| "setupQueries": [ | ||
| "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", | ||
| "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd388602654d094c34e2c718a802d0243')" | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Room 데이터베이스 스키마가 적절히 정의되었습니다.
OpenLectureEntity 테이블 스키마가 필요한 모든 필드를 포함하여 잘 정의되었습니다. number를 PRIMARY KEY로 사용하는 것이 적절합니다.
몇 가지 확인이 필요한 사항이 있습니다:
- OpenLectureRaw 클래스에서는 grade가 Int 타입인데, 스키마에서는 TEXT로 정의되었습니다. 이 부분이 의도적인지 확인해주세요.
- 자주 쿼리되는 필드(예: major, className, professor)에 대한 인덱스가 없습니다. 필요하다면 성능 향상을 위해 인덱스 추가를 고려해보세요.
🏁 Script executed:
#!/bin/bash
# 관련 엔티티 클래스 찾기
echo "Searching for OpenLectureEntity definition:"
rg -A 10 "class OpenLectureEntity"
# grade 필드 타입 확인
echo "Checking grade field type in OpenLectureEntity:"
rg "val grade:" --type kotlinLength of output: 2021
Room 데이터베이스 스키마 검토 – 매핑 불일치 확인 및 수정 필요
Room 스키마와 엔티티 간 불일치 사항이 확인되었습니다. 아래 항목을 검토해주세요:
- grade 타입 불일치
엔티티(Int) vs 스키마(TEXT)
→INTEGER로 통일하거나 컬럼 매핑을 명시적으로 지정해주세요. - 컬럼 이름 불일치 (
numbervsid)
스키마에서number를 PK로 사용하지만, 엔티티는id: Long을 PK로 정의
→ @ColumnInfo(name = "number") 추가 또는 스키마 업데이트를 검토해주세요. - 인덱스 부재
major,className,professor등 자주 조회되는 컬럼에 인덱스가 없어 성능 저하 우려
→ 필요한 경우 인덱스 추가를 고려해주세요.
대상 파일:
- schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json
- local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt
| @Entity | ||
| @Serializable | ||
| data class CellEntity( | ||
| val location: String, | ||
| val day: TimetableDay, | ||
| val startPeriod: Int, | ||
| val endPeriod: Int | ||
| ) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
CellEntity 정의가 적절합니다.
CellEntity가 적절히 정의되었고, @Serializable 애노테이션이 올바르게 적용되었습니다. 이는 JSON 직렬화/역직렬화를 위해 필요합니다.
다만, @Entity 애노테이션이 CellEntity에 적용되어 있는데, 이 클래스가 별도의 테이블로 사용되는 것인지 확인이 필요합니다. OpenLectureEntity의 일부로만 사용된다면 @entity 애노테이션은 필요하지 않을 수 있습니다.
CellEntity가 독립적인 테이블로 사용되나요? 아니면 단지 OpenLectureEntity 내에 JSON으로 저장되는 객체인가요? 다음 스크립트로 확인해볼 수 있습니다:
🏁 Script executed:
#!/bin/bash
# Room 데이터베이스 스키마에서 CellEntity 테이블 존재 여부 확인
find schemas -name "*.json" -exec grep -l "CellEntity" {} \;
# OpenLectureConverter 코드에서 CellEntity 직렬화/역직렬화 확인
find . -name "OpenLectureConverter.kt" -exec cat {} \;Length of output: 664
CellEntity의 @entity 애노테이션 제거 필요
CellEntity는 OpenLectureEntity 내 List를 JSON 문자열로 저장/읽어오기 위한 타입 변환용 객체이며, 스키마(JSON)나 쿼리에서 별도 테이블로 사용되지 않습니다. 따라서 아래 파일에서 @Entity 애노테이션을 제거해주세요:
- local/common/src/main/java/com/suwiki/local/common/database/entity/CellEntity.kt
| { | ||
| "formatVersion": 1, | ||
| "database": { | ||
| "version": 2, | ||
| "identityHash": "d388602654d094c34e2c718a802d0243", | ||
| "entities": [ | ||
| { | ||
| "tableName": "OpenLectureEntity", | ||
| "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`number` INTEGER NOT NULL, `major` TEXT NOT NULL, `grade` TEXT NOT NULL, `classNumber` TEXT NOT NULL, `classDivideNumber` TEXT NOT NULL, `className` TEXT NOT NULL, `classification` TEXT NOT NULL, `professor` TEXT NOT NULL, `time` TEXT NOT NULL, `credit` TEXT NOT NULL, PRIMARY KEY(`number`))", | ||
| "fields": [ | ||
| { | ||
| "fieldPath": "number", | ||
| "columnName": "number", | ||
| "affinity": "INTEGER", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "major", | ||
| "columnName": "major", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "grade", | ||
| "columnName": "grade", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classNumber", | ||
| "columnName": "classNumber", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classDivideNumber", | ||
| "columnName": "classDivideNumber", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "className", | ||
| "columnName": "className", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "classification", | ||
| "columnName": "classification", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "professor", | ||
| "columnName": "professor", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "time", | ||
| "columnName": "time", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| }, | ||
| { | ||
| "fieldPath": "credit", | ||
| "columnName": "credit", | ||
| "affinity": "TEXT", | ||
| "notNull": true | ||
| } | ||
| ], | ||
| "primaryKey": { | ||
| "autoGenerate": false, | ||
| "columnNames": [ | ||
| "number" | ||
| ] | ||
| }, | ||
| "indices": [], | ||
| "foreignKeys": [] | ||
| } | ||
| ], | ||
| "views": [], | ||
| "setupQueries": [ | ||
| "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", | ||
| "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd388602654d094c34e2c718a802d0243')" | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
중복된 스키마 파일이 발견되었습니다.
이 파일은 schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json와 내용이 동일합니다. 같은 데이터베이스에 대한 스키마 파일이 두 위치에 존재하는 것으로 보입니다.
경로 중 하나가 잘못된 것일 수 있으니 확인이 필요합니다. Room 데이터베이스는 일반적으로 하나의 경로에만 스키마 파일을 생성합니다.
| override suspend fun getOpenLectureList(): List<OpenLectureRaw> = firebaseDatabase | ||
| .getReference(DATABASE_OPEN_LECTURE) | ||
| .get() | ||
| .await() | ||
| .children | ||
| .mapIndexed { index, dataSnapshot -> | ||
| val data = dataSnapshot.value as HashMap<*, *> | ||
| OpenLectureRaw( | ||
| number = index.toLong() + 1, | ||
| major = data[FIELD_MAJOR].toString(), | ||
| grade = data[FIELD_GRADE].toString().toIntOrNull() ?: 1, | ||
| className = data[FIELD_CLASS_NAME].toString(), | ||
| classification = data[FIELD_CLASSIFICATION].toString(), | ||
| professor = data[FIELD_PROFESSOR]?.toString() ?: DEFAULT, | ||
| time = data[FIELD_TIME]?.toString() ?: DEFAULT, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Firebase 데이터 매핑 처리에 개선이 필요합니다.
Firebase에서 데이터를 가져와 OpenLectureRaw로 매핑하는 로직은 잘 구현되었으나, 예외 처리가 부족합니다.
데이터 매핑 과정에서 발생할 수 있는 예외 상황(데이터 형식 불일치, null 값 등)에 대한 추가 처리가 필요합니다. 다음과 같이 try-catch 블록을 추가하여 안정성을 높이세요:
- override suspend fun getOpenLectureList(): List<OpenLectureRaw> = firebaseDatabase
+ override suspend fun getOpenLectureList(): List<OpenLectureRaw> = try {
+ firebaseDatabase
.getReference(DATABASE_OPEN_LECTURE)
.get()
.await()
.children
.mapIndexed { index, dataSnapshot ->
- val data = dataSnapshot.value as HashMap<*, *>
+ val data = dataSnapshot.value as? HashMap<*, *> ?: hashMapOf<String, Any>()
OpenLectureRaw(
number = index.toLong() + 1,
major = data[FIELD_MAJOR].toString(),
grade = data[FIELD_GRADE].toString().toIntOrNull() ?: 1,
className = data[FIELD_CLASS_NAME].toString(),
classification = data[FIELD_CLASSIFICATION].toString(),
professor = data[FIELD_PROFESSOR]?.toString() ?: DEFAULT,
time = data[FIELD_TIME]?.toString() ?: DEFAULT,
)
}
+ } catch (e: Exception) {
+ // Firebase 데이터 불러오기 실패 로깅
+ emptyList()
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override suspend fun getOpenLectureList(): List<OpenLectureRaw> = firebaseDatabase | |
| .getReference(DATABASE_OPEN_LECTURE) | |
| .get() | |
| .await() | |
| .children | |
| .mapIndexed { index, dataSnapshot -> | |
| val data = dataSnapshot.value as HashMap<*, *> | |
| OpenLectureRaw( | |
| number = index.toLong() + 1, | |
| major = data[FIELD_MAJOR].toString(), | |
| grade = data[FIELD_GRADE].toString().toIntOrNull() ?: 1, | |
| className = data[FIELD_CLASS_NAME].toString(), | |
| classification = data[FIELD_CLASSIFICATION].toString(), | |
| professor = data[FIELD_PROFESSOR]?.toString() ?: DEFAULT, | |
| time = data[FIELD_TIME]?.toString() ?: DEFAULT, | |
| ) | |
| } | |
| override suspend fun getOpenLectureList(): List<OpenLectureRaw> = try { | |
| firebaseDatabase | |
| .getReference(DATABASE_OPEN_LECTURE) | |
| .get() | |
| .await() | |
| .children | |
| .mapIndexed { index, dataSnapshot -> | |
| val data = dataSnapshot.value as? HashMap<*, *> ?: hashMapOf<String, Any>() | |
| OpenLectureRaw( | |
| number = index.toLong() + 1, | |
| major = data[FIELD_MAJOR].toString(), | |
| grade = data[FIELD_GRADE].toString().toIntOrNull() ?: 1, | |
| className = data[FIELD_CLASS_NAME].toString(), | |
| classification = data[FIELD_CLASSIFICATION].toString(), | |
| professor = data[FIELD_PROFESSOR]?.toString() ?: DEFAULT, | |
| time = data[FIELD_TIME]?.toString() ?: DEFAULT, | |
| ) | |
| } | |
| } catch (e: Exception) { | |
| // Firebase 데이터 불러오기 실패 로깅 | |
| emptyList() | |
| } |
| override suspend fun updateAllLectures() = coroutineScope { | ||
| val remoteOpenLectures = async { | ||
| remoteOpenLectureDataSource.getOpenLectureList().map { | ||
| OpenLecture( | ||
| id = it.number, | ||
| name = it.className, | ||
| type = it.classification, | ||
| major = it.major, | ||
| grade = it.grade, | ||
| professorName = it.professor, | ||
| originalCellList = TimetableUtil.parseTimeTableString(it.time), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| val remoteOpenLectureVersion = async { | ||
| remoteOpenLectureDataSource.getOpenLectureListVersion() | ||
| } | ||
|
|
||
| localOpenLectureDataSource.updateAllLectures(remoteOpenLectures.await()) | ||
| localOpenLectureDataSource.setOpenLectureListVersion(remoteOpenLectureVersion.await()) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
원격 버전·목록 2중 호출로 인한 오버헤드
async 블록을 두 번 사용해 동일 원격 경로를 두 번 조회하면 지연 시간이 늘고, 과금이 있는 Firebase 사용 시 비용이 증가할 수 있습니다. 버전과 목록을 한 번의 호출로 가져오거나, 목록 응답에 포함된 타임스탬프를 재사용하는 구조로 리팩터링을 고려해 주세요.
| override suspend fun checkNeedUpdate(): Boolean { | ||
| val localVersion = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull() ?: return true | ||
| val remoteVersion = remoteOpenLectureDataSource.getOpenLectureListVersion() | ||
| return remoteVersion > localVersion | ||
| } |
There was a problem hiding this comment.
네트워크 예외 처리 누락
remoteOpenLectureDataSource.getOpenLectureListVersion() 호출 실패 시 그대로 예외가 전파되어 앱이 종료될 수 있습니다. 안전한 사용자 경험을 위해 runCatching 또는 자체 에러 핸들러로 감싸고, 실패 시 true(갱신 필요) 또는 이전 캐시 사용 등 graceful-fallback 로직을 추가해주세요.
- val remoteVersion = remoteOpenLectureDataSource.getOpenLectureListVersion()
- return remoteVersion > localVersion
+ val remoteVersion = runCatching { remoteOpenLectureDataSource.getOpenLectureListVersion() }
+ .getOrElse { return true } // 네트워크 실패 시 업데이트 필요로 간주
+ return remoteVersion > localVersion📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override suspend fun checkNeedUpdate(): Boolean { | |
| val localVersion = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull() ?: return true | |
| val remoteVersion = remoteOpenLectureDataSource.getOpenLectureListVersion() | |
| return remoteVersion > localVersion | |
| } | |
| override suspend fun checkNeedUpdate(): Boolean { | |
| val localVersion = localOpenLectureDataSource | |
| .getOpenLectureListVersion() | |
| .firstOrNull() ?: return true | |
| val remoteVersion = runCatching { | |
| remoteOpenLectureDataSource.getOpenLectureListVersion() | |
| } | |
| .getOrElse { return true } // 네트워크 실패 시 업데이트 필요로 간주 | |
| return remoteVersion > localVersion | |
| } |
| private fun getOpenLectureList( | ||
| search: String = searchQuery, | ||
| needClear: Boolean, | ||
| ) = intent { | ||
| ) = intent { | ||
| mutex.withLock { | ||
| val currentList = when { | ||
| needClear -> { | ||
| reduce { state.copy(isLoading = true) } | ||
| cursorId = 0 | ||
| isLast = false | ||
| emptyList() | ||
| } | ||
|
|
||
| isLast -> return@intent | ||
| else -> state.openLectureList | ||
| } | ||
|
|
||
| getOpenLectureListUseCase( | ||
| val newData = getOpenLectureListUseCase( | ||
| GetOpenLectureListUseCase.Param( | ||
| cursorId = cursorId, | ||
| keyword = search, | ||
| lectureOrProfessorName = search, | ||
| major = if (currentState.selectedOpenMajor == "전체") null else currentState.selectedOpenMajor, | ||
| grade = currentState.schoolLevel.query, | ||
| ), | ||
| ).onSuccess { newData -> | ||
| handleGetOpenLectureListSuccess( | ||
| currentList = currentList, | ||
| newData = newData, | ||
| ) | ||
| }.onFailure { | ||
| ).catch { | ||
| postSideEffect(OpenLectureSideEffect.HandleException(it)) | ||
| }.firstOrNull() ?: return@withLock | ||
|
|
||
| reduce { | ||
| state.copy( | ||
| isLoading = false, | ||
| openLectureList = newData | ||
| .distinctBy { it.id } | ||
| .toPersistentList(), | ||
| ) | ||
| } | ||
|
|
||
| if (needClear) { | ||
| postSideEffect(OpenLectureSideEffect.ScrollToTop) | ||
| reduce { state.copy(isLoading = false) } | ||
| } | ||
| postSideEffect(OpenLectureSideEffect.ScrollToTop) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
리스트 갱신 로직 최적화 제안
reduce { state.copy(isLoading = true) }를 호출해 사용자에게 검색/필터 로딩을 명시적으로 알려주세요.firstOrNull()은 스트림의 첫 값만 사용해 이후 Firebase/Room 변경을 무시합니다. 장기적으로는collectLatest { … }로 무한 스트림을 수신하여 실시간 갱신을 반영하는 것이 캐시 전략과 일관됩니다.Mutex는 ORBIT 의 단일 스레드-컨텍스트intent안에서는 대부분 불필요합니다. 삭제 후 동작을 재검토해 보세요.
| GetOpenLectureListUseCase.Param( | ||
| cursorId = cursorId, | ||
| keyword = search, | ||
| lectureOrProfessorName = search, | ||
| major = if (currentState.selectedOpenMajor == "전체") null else currentState.selectedOpenMajor, | ||
| grade = currentState.schoolLevel.query, | ||
| ), | ||
| ).onSuccess { newData -> | ||
| handleGetOpenLectureListSuccess( | ||
| currentList = currentList, | ||
| newData = newData, | ||
| ) | ||
| }.onFailure { | ||
| ).catch { | ||
| postSideEffect(OpenLectureSideEffect.HandleException(it)) |
There was a problem hiding this comment.
플로우 에러 처리 시 isLoading 해제를 잊지 마세요
.catch { … } 블록 안에서 토스트만 띄우고 있고, reduce { state.copy(isLoading = false) } 가 호출되지 않습니다.
네트워크 실패 후 UI 가 계속 로딩 상태로 남습니다.
).catch {
postSideEffect(OpenLectureSideEffect.HandleException(it))
+ reduce { state.copy(isLoading = false) }
}.firstOrNull() ?: return@withLock| fun initData() = intent { | ||
| if (isFirstVisit) { | ||
| reduce { state.copy(isLoading = true) } | ||
| updateOpenLectureIfNeedUseCase() | ||
| val lastUpdated = openLectureRepository.getLastUpdatedDate() | ||
| reduce { | ||
| state.copy( | ||
| lastUpdatedDate = lastUpdated, | ||
| ) | ||
| } | ||
| getOpenLectureList() | ||
| isFirstVisit = false | ||
| } | ||
| } |
There was a problem hiding this comment.
initData() 예외 처리 누락 및 로딩 플래그 미정리
updateOpenLectureIfNeedUseCase()실패 시 예외가 ViewModel 을 터뜨릴 가능성이 있습니다.- 실패 시
isLoading이true로 남아 UI 가 영구적으로 스켈레톤을 표시할 수 있습니다.
아래와 같이 runCatching(또는 try/catch) 블록으로 감싸고, 실패 시에도 로딩 플래그를 해제해 주세요.
- reduce { state.copy(isLoading = true) }
- updateOpenLectureIfNeedUseCase()
- val lastUpdated = openLectureRepository.getLastUpdatedDate()
- reduce {
- state.copy(
- lastUpdatedDate = lastUpdated,
- )
- }
+ reduce { state.copy(isLoading = true) }
+
+ kotlin.runCatching {
+ updateOpenLectureIfNeedUseCase()
+ openLectureRepository.getLastUpdatedDate()
+ }.onSuccess { lastUpdated ->
+ reduce { state.copy(lastUpdatedDate = lastUpdated) }
+ }.onFailure {
+ postSideEffect(OpenLectureSideEffect.HandleException(it))
+ }Committable suggestion skipped: line range outside the PR's diff.
| "목" -> TimetableDay.THU | ||
| "금" -> TimetableDay.FRI | ||
| "토" -> TimetableDay.SAT | ||
| else -> throw IllegalArgumentException("Invalid day: $day") |
There was a problem hiding this comment.
0f80dbf#diff-499cc6459302a199d0e9dd8e67ce0caa373dce6e8f16563d39fdc154517f0dd4
참고 커밋에 해당 로직이 있어서 추가 했습니다
📌 PR 요약
🌱 작업한 내용
🌱 PR 포인트
📸 스크린샷
📮 관련 이슈
Summary by CodeRabbit
신규 기능
개선 및 변경
버그 수정
기타