From 8fc24942682720c56f1448a02337ba2ce4e6114a Mon Sep 17 00:00:00 2001 From: BEEEAM-J Date: Fri, 9 May 2025 23:29:23 +0900 Subject: [PATCH] =?UTF-8?q?#178=20feat:=20=EA=B0=9C=EC=84=A4=ED=95=99?= =?UTF-8?q?=EA=B3=BC=20firebase=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0f80dbf commit을 develop branch에 맞게 적용 --- data/timetable/build.gradle.kts | 1 + .../suwiki/data/timetable/OpenLectureRaw.kt | 11 +++ .../suwiki/data/timetable/TimetableUtil.kt | 58 +++++++++++ .../datasource/LocalOpenLectureDataSource.kt | 18 ++++ .../datasource/RemoteOpenLectureDataSource.kt | 11 +-- .../repository/OpenLectureRepositoryImpl.kt | 85 +++++++++++++--- .../repository/OpenLectureRepository.kt | 23 +++-- .../usecase/GetOpenLectureListUseCase.kt | 20 ++-- .../usecase/UpdateOpenLectureIfNeedUseCase.kt | 14 +++ gradle/libs.versions.toml | 3 +- .../TimetableDatabaseMigrate1To2Test.kt | 2 +- .../converter/OpenLectureConverter.kt | 20 ++++ .../common/database/dao/OpenLectureDao.kt | 39 ++++++++ .../database/database/OpenLectureDatabase.kt | 15 +++ .../{ => database}/OpenMajorDatabase.kt | 2 +- .../{ => database}/TimetableDatabase.kt | 2 +- .../local/common/database/di/DaoModule.kt | 9 +- .../common/database/di/DatabaseModule.kt | 16 ++- .../database/entity/OpenLectureEntity.kt | 27 +++++ .../LocalOpenMajorDataSourceImpl.kt | 2 +- .../converter/OpenLectureEntity.kt | 40 ++++++++ .../LocalOpenLectureDatasourceImpl.kt | 62 ++++++++++++ .../LocalTimetableDatasourceImpl.kt | 2 +- .../di/LocalDataSourceModule.kt | 8 ++ presentation/openmajor/build.gradle.kts | 1 + .../openmajor/OpenMajorViewModel.kt | 5 +- .../openlecture/OpenLectureContract.kt | 1 + .../openlecture/OpenLectureScreen.kt | 15 ++- .../openlecture/OpenLectureViewModel.kt | 99 ++++++++----------- remote/common/build.gradle.kts | 6 +- .../common/di/FirebaseDatabaseModule.kt | 19 ++++ remote/timetable/build.gradle.kts | 4 + .../RemoteOpenLectureDataSourceImpl.kt | 63 +++++++++--- .../1.json | 88 +++++++++++++++++ .../2.json | 88 +++++++++++++++++ .../1.json | 66 +++++++++---- .../1.json | 70 +++++++++++++ .../2.json | 88 +++++++++++++++++ .../1.json | 40 ++++++++ .../2.json | 40 ++++++++ .../1.json | 58 +++++++++++ .../2.json | 2 +- 42 files changed, 1091 insertions(+), 152 deletions(-) create mode 100644 data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt create mode 100644 data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt create mode 100644 data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt create mode 100644 domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt create mode 100644 local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt create mode 100644 local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt create mode 100644 local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt rename local/common/src/main/java/com/suwiki/local/common/database/{ => database}/OpenMajorDatabase.kt (93%) rename local/common/src/main/java/com/suwiki/local/common/database/{ => database}/TimetableDatabase.kt (91%) create mode 100644 local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt create mode 100644 local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt create mode 100644 local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt create mode 100644 remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt create mode 100644 schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json create mode 100644 schemas/com.suwiki.local.common.database.OpenLectureDatabase/2.json create mode 100644 schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json create mode 100644 schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json create mode 100644 schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json create mode 100644 schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json create mode 100644 schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json rename schemas/{com.suwiki.local.common.database.TimetableDatabase => com.suwiki.local.common.database.database.TimetableDatabase}/2.json (99%) diff --git a/data/timetable/build.gradle.kts b/data/timetable/build.gradle.kts index 8622a6523..b4fc7fa1d 100644 --- a/data/timetable/build.gradle.kts +++ b/data/timetable/build.gradle.kts @@ -9,4 +9,5 @@ android { dependencies { implementation(projects.domain.timetable) + testImplementation(libs.junit4) } diff --git a/data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt b/data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt new file mode 100644 index 000000000..6517ff0cb --- /dev/null +++ b/data/timetable/src/main/java/com/suwiki/data/timetable/OpenLectureRaw.kt @@ -0,0 +1,11 @@ +package com.suwiki.data.timetable + +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 = "", +) diff --git a/data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt b/data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt new file mode 100644 index 000000000..5f593898e --- /dev/null +++ b/data/timetable/src/main/java/com/suwiki/data/timetable/TimetableUtil.kt @@ -0,0 +1,58 @@ +package com.suwiki.data.timetable + +import com.suwiki.common.model.timetable.Cell +import com.suwiki.common.model.timetable.TimetableDay + +object TimetableUtil { + fun parseTimeTableString(input: String): List { + val cellRegex = """([^(]+)\(([^)]+)\)""".toRegex() + val dayPeriodRegex = """([월화수목금토])([\d,]+)""".toRegex() + + val result = input.split("),").flatMap { cellInput -> + val sanitizedInput = if (!cellInput.endsWith(")")) "$cellInput)" else cellInput + cellRegex.find(sanitizedInput)?.let { cellMatch -> + val (location, schedule) = cellMatch.destructured + val trimmedLocation = location.trim() + dayPeriodRegex.findAll(schedule).flatMap { dayPeriodMatch -> + val (day, periods) = dayPeriodMatch.destructured + val periodList = periods.split(",").map { it.toInt() } + + periodList.groupConsecutive().map { (start, end) -> + Cell( + location = trimmedLocation, + day = when (day) { + "월" -> TimetableDay.MON + "화" -> TimetableDay.TUE + "수" -> TimetableDay.WED + "목" -> TimetableDay.THU + "금" -> TimetableDay.FRI + "토" -> TimetableDay.SAT + else -> throw IllegalArgumentException("Invalid day: $day") + }, + startPeriod = start, + endPeriod = end + ) + } + }.toList() + } ?: emptyList() + } + + return result + } + + private fun List.groupConsecutive(): List> { + if (isEmpty()) return emptyList() + val result = mutableListOf>() + var start = this[0] + var prev = start + for (i in 1 until size) { + if (this[i] != prev + 1) { + result.add(start to prev) + start = this[i] + } + prev = this[i] + } + result.add(start to prev) + return result + } +} diff --git a/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt b/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt new file mode 100644 index 000000000..433e9c069 --- /dev/null +++ b/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/LocalOpenLectureDataSource.kt @@ -0,0 +1,18 @@ +package com.suwiki.data.timetable.datasource + +import com.suwiki.common.model.timetable.OpenLecture +import kotlinx.coroutines.flow.Flow + +interface LocalOpenLectureDataSource { + fun getOpenLectureListVersion(): Flow + + suspend fun setOpenLectureListVersion(version: Long) + + fun getOpenLectureList( + lectureOrProfessorName: String? = null, + major: String? = null, + grade: Int? = null + ): Flow> + + suspend fun updateAllLectures(lectures: List) +} diff --git a/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt b/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt index 6adb0603b..2e5c215a3 100644 --- a/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt +++ b/data/timetable/src/main/java/com/suwiki/data/timetable/datasource/RemoteOpenLectureDataSource.kt @@ -1,13 +1,8 @@ package com.suwiki.data.timetable.datasource -import com.suwiki.common.model.timetable.OpenLectureData +import com.suwiki.data.timetable.OpenLectureRaw interface RemoteOpenLectureDataSource { - suspend fun getOpenLectureList( - cursorId: Long, - size: Long, - keyword: String?, - major: String?, - grade: Int?, - ): OpenLectureData + suspend fun getOpenLectureListVersion(): Long + suspend fun getOpenLectureList(): List } diff --git a/data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt b/data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt index 176452fe5..d9bfe0d14 100644 --- a/data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt +++ b/data/timetable/src/main/java/com/suwiki/data/timetable/repository/OpenLectureRepositoryImpl.kt @@ -1,24 +1,85 @@ package com.suwiki.data.timetable.repository -import com.suwiki.common.model.timetable.OpenLectureData +import com.suwiki.common.model.timetable.OpenLecture +import com.suwiki.data.timetable.TimetableUtil +import com.suwiki.data.timetable.datasource.LocalOpenLectureDataSource import com.suwiki.data.timetable.datasource.RemoteOpenLectureDataSource import com.suwiki.domain.timetable.repository.OpenLectureRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale import javax.inject.Inject class OpenLectureRepositoryImpl @Inject constructor( private val remoteOpenLectureDataSource: RemoteOpenLectureDataSource, + private val localOpenLectureDataSource: LocalOpenLectureDataSource, ) : OpenLectureRepository { - override suspend fun getOpenLectureList( - cursorId: Long, - size: Long, - keyword: String?, + override fun getOpenLectureList( + lectureOrProfessorName: String?, + + major: String?, grade: Int?, - ): OpenLectureData = remoteOpenLectureDataSource.getOpenLectureList( - cursorId = cursorId, - size = size, - keyword = keyword, - major = major, - grade = grade, - ) + ): Flow> { + return localOpenLectureDataSource.getOpenLectureList( + lectureOrProfessorName = lectureOrProfessorName, + major = major, + grade = grade, + ) + } + + override suspend fun checkNeedUpdate(): Boolean { + val localVersion = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull() ?: return true + val remoteVersion = remoteOpenLectureDataSource.getOpenLectureListVersion() + return remoteVersion > localVersion + } + + 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()) + } + + override suspend fun getLastUpdatedDate(): String? { + return try { + val version = localOpenLectureDataSource.getOpenLectureListVersion().firstOrNull().toString() + if (version.length != 12) { + return null + } + + val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm") + val dateTime = LocalDateTime.parse(version, formatter) + + val koreanFormatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일 H시 m분", Locale.KOREAN) + dateTime.format(koreanFormatter) + } catch (e: Exception) { + null + } + } + + override suspend fun getOpenMajor(): List { + return localOpenLectureDataSource.getOpenLectureList().map { list -> list.map { it.major } }.firstOrNull() ?: emptyList() + } } diff --git a/domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt b/domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt index a07ab82d0..0b63b6675 100644 --- a/domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt +++ b/domain/timetable/src/main/java/com/suwiki/domain/timetable/repository/OpenLectureRepository.kt @@ -1,13 +1,20 @@ package com.suwiki.domain.timetable.repository -import com.suwiki.common.model.timetable.OpenLectureData +import com.suwiki.common.model.timetable.OpenLecture +import kotlinx.coroutines.flow.Flow interface OpenLectureRepository { - suspend fun getOpenLectureList( - cursorId: Long, - size: Long, - keyword: String?, - major: String?, - grade: Int?, - ): OpenLectureData + fun getOpenLectureList( + lectureOrProfessorName: String? = null, + major: String? = null, + grade: Int? = null, + ): Flow> + + suspend fun checkNeedUpdate(): Boolean + + suspend fun updateAllLectures() + + suspend fun getLastUpdatedDate(): String? + + suspend fun getOpenMajor(): List } diff --git a/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt b/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt index d8a4848c6..7a5fd1a6f 100644 --- a/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt +++ b/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/GetOpenLectureListUseCase.kt @@ -8,22 +8,18 @@ import javax.inject.Inject class GetOpenLectureListUseCase @Inject constructor( private val openLectureRepository: OpenLectureRepository, ) { - suspend operator fun invoke(param: Param): Result = runCatchingIgnoreCancelled { + operator fun invoke(param: Param) = with(param) { - openLectureRepository.getOpenLectureList( - cursorId = cursorId, - size = size, - keyword = keyword, - major = major, - grade = grade, - ) + openLectureRepository + .getOpenLectureList( + lectureOrProfessorName = lectureOrProfessorName, + major = major, + grade = grade, + ) } - } data class Param( - val cursorId: Long, - val size: Long = 20, - val keyword: String?, + val lectureOrProfessorName: String?, val major: String?, val grade: Int?, ) diff --git a/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt b/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt new file mode 100644 index 000000000..c007e2c39 --- /dev/null +++ b/domain/timetable/src/main/java/com/suwiki/domain/timetable/usecase/UpdateOpenLectureIfNeedUseCase.kt @@ -0,0 +1,14 @@ +package com.suwiki.domain.timetable.usecase + +import com.suwiki.domain.common.runCatchingIgnoreCancelled +import com.suwiki.domain.timetable.repository.OpenLectureRepository +import javax.inject.Inject + +class UpdateOpenLectureIfNeedUseCase @Inject constructor( + private val openLectureRepository: OpenLectureRepository, +) { + suspend operator fun invoke(): Result = runCatchingIgnoreCancelled { + if(openLectureRepository.checkNeedUpdate().not()) return@runCatchingIgnoreCancelled + openLectureRepository.updateAllLectures() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e33696ab6..990493883 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -179,6 +179,7 @@ encrypted-datastore-preference-security = { group = "tech.thdev", name = "useful firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } +firebase-database = { group = "com.google.firebase", name = "firebase-database-ktx" } ted-permission = { group = "io.github.ParkSangGwon", name = "tedpermission-normal", version.ref = "ted-permission" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } @@ -204,7 +205,7 @@ kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.re androidx-core = { group = "androidx.test", name = "core", version.ref = "core" } [bundles] -firebase = ["firebase-analytics"] +firebase = ["firebase-analytics", "firebase-database"] androidx-lifecycle = ["androidx-lifecycle-runtime-ktx", "androidx-lifecycle-viewmodel-ktx"] androidx-navigation = ["navigation-fragment-ktx", "navigation-ui-ktx"] coroutine = ["kotlinx-coroutines-android", "kotlinx-coroutines-core"] diff --git a/local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt b/local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt index 4f64d9866..7fa629aae 100644 --- a/local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt +++ b/local/common/src/androidTest/java/com/suwiki/local/common/TimetableDatabaseMigrate1To2Test.kt @@ -3,7 +3,7 @@ package com.suwiki.local.common import androidx.room.testing.MigrationTestHelper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.suwiki.local.common.database.TimetableDatabase +import com.suwiki.local.common.database.database.TimetableDatabase import com.suwiki.local.common.database.di.DatabaseName import com.suwiki.local.common.database.migration.TIMETABLE_MIGRATION_1_2 import org.junit.Rule diff --git a/local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt b/local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt new file mode 100644 index 000000000..d6519028b --- /dev/null +++ b/local/common/src/main/java/com/suwiki/local/common/database/converter/OpenLectureConverter.kt @@ -0,0 +1,20 @@ +package com.suwiki.local.common.database.converter + +import androidx.room.TypeConverter +import com.suwiki.local.common.database.entity.CellEntity +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class OpenLectureConverter { + private val json = Json { ignoreUnknownKeys = true } + + @TypeConverter + fun fromCellList(value: List): String { + return json.encodeToString(value) + } + + @TypeConverter + fun toCellList(value: String): List { + return json.decodeFromString(value) + } +} diff --git a/local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt b/local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt new file mode 100644 index 000000000..5b364abc5 --- /dev/null +++ b/local/common/src/main/java/com/suwiki/local/common/database/dao/OpenLectureDao.kt @@ -0,0 +1,39 @@ +package com.suwiki.local.common.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.suwiki.local.common.database.entity.OpenLectureEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface OpenLectureDao { + + @Query( + """ + SELECT * FROM OpenLectureEntity + WHERE (:lectureOrProfessorName IS NULL OR name LIKE '%' || :lectureOrProfessorName || '%' OR professorName LIKE '%' || :lectureOrProfessorName || '%') + AND (:major IS NULL OR major LIKE '%' || :major || '%') + AND (:grade IS NULL OR grade = :grade) + """ + ) + fun searchLectures( + lectureOrProfessorName: String? = null, + major: String? = null, + grade: Int? = null + ): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLectures(lectures: List) + + @Query("DELETE FROM OpenLectureEntity") + suspend fun deleteAllLectures() + + @Transaction + suspend fun updateAllLectures(lectures: List) { + deleteAllLectures() + insertLectures(lectures) + } +} diff --git a/local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt b/local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt new file mode 100644 index 000000000..6cbbf6927 --- /dev/null +++ b/local/common/src/main/java/com/suwiki/local/common/database/database/OpenLectureDatabase.kt @@ -0,0 +1,15 @@ +package com.suwiki.local.common.database.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.suwiki.local.common.database.converter.OpenLectureConverter +import com.suwiki.local.common.database.dao.OpenLectureDao +import com.suwiki.local.common.database.entity.OpenLectureEntity + + +@Database(entities = [OpenLectureEntity::class], version = 1) +@TypeConverters(OpenLectureConverter::class) +abstract class OpenLectureDatabase : RoomDatabase() { + abstract fun openLectureDao(): OpenLectureDao +} diff --git a/local/common/src/main/java/com/suwiki/local/common/database/OpenMajorDatabase.kt b/local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt similarity index 93% rename from local/common/src/main/java/com/suwiki/local/common/database/OpenMajorDatabase.kt rename to local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt index 005fc3fd5..2024ddd80 100644 --- a/local/common/src/main/java/com/suwiki/local/common/database/OpenMajorDatabase.kt +++ b/local/common/src/main/java/com/suwiki/local/common/database/database/OpenMajorDatabase.kt @@ -1,4 +1,4 @@ -package com.suwiki.local.common.database +package com.suwiki.local.common.database.database import androidx.room.AutoMigration import androidx.room.Database diff --git a/local/common/src/main/java/com/suwiki/local/common/database/TimetableDatabase.kt b/local/common/src/main/java/com/suwiki/local/common/database/database/TimetableDatabase.kt similarity index 91% rename from local/common/src/main/java/com/suwiki/local/common/database/TimetableDatabase.kt rename to local/common/src/main/java/com/suwiki/local/common/database/database/TimetableDatabase.kt index 0ee1eed0e..f7c99765c 100644 --- a/local/common/src/main/java/com/suwiki/local/common/database/TimetableDatabase.kt +++ b/local/common/src/main/java/com/suwiki/local/common/database/database/TimetableDatabase.kt @@ -1,4 +1,4 @@ -package com.suwiki.local.common.database +package com.suwiki.local.common.database.database import androidx.room.Database import androidx.room.RoomDatabase diff --git a/local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt b/local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt index fe34c77c1..8b2a37545 100644 --- a/local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt +++ b/local/common/src/main/java/com/suwiki/local/common/database/di/DaoModule.kt @@ -1,9 +1,11 @@ package com.suwiki.local.common.database.di -import com.suwiki.local.common.database.OpenMajorDatabase -import com.suwiki.local.common.database.TimetableDatabase +import com.suwiki.local.common.database.dao.OpenLectureDao +import com.suwiki.local.common.database.database.OpenMajorDatabase +import com.suwiki.local.common.database.database.TimetableDatabase import com.suwiki.local.common.database.dao.OpenMajorDao import com.suwiki.local.common.database.dao.TimeTableDao +import com.suwiki.local.common.database.database.OpenLectureDatabase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,4 +20,7 @@ object DaoModule { @Provides fun provideTimetableDao(db: TimetableDatabase): TimeTableDao = db.timetableDao() + + @Provides + fun provideOpenLectureDao(db: OpenLectureDatabase): OpenLectureDao = db.openLectureDao() } diff --git a/local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt b/local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt index 279e36ad1..61146dfa8 100644 --- a/local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt +++ b/local/common/src/main/java/com/suwiki/local/common/database/di/DatabaseModule.kt @@ -2,8 +2,9 @@ package com.suwiki.local.common.database.di import android.content.Context import androidx.room.Room -import com.suwiki.local.common.database.OpenMajorDatabase -import com.suwiki.local.common.database.TimetableDatabase +import com.suwiki.local.common.database.database.OpenLectureDatabase +import com.suwiki.local.common.database.database.OpenMajorDatabase +import com.suwiki.local.common.database.database.TimetableDatabase import com.suwiki.local.common.database.migration.TIMETABLE_MIGRATION_1_2 import dagger.Module import dagger.Provides @@ -46,9 +47,20 @@ object DatabaseModule { .fallbackToDestructiveMigration() .build() } + + @Provides + @Singleton + fun provideOpenLectureDatabase(@ApplicationContext context: Context): OpenLectureDatabase { + return Room.databaseBuilder( + context, + OpenLectureDatabase::class.java, + DatabaseName.OPEN_LECTURE + ).build() + } } object DatabaseName { const val OPEN_MAJOR = "open-major-database" const val TIMETABLE = "timetable-list-database" + const val OPEN_LECTURE = "open-lecture-database" } diff --git a/local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt b/local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt new file mode 100644 index 000000000..cfdc79e14 --- /dev/null +++ b/local/common/src/main/java/com/suwiki/local/common/database/entity/OpenLectureEntity.kt @@ -0,0 +1,27 @@ +package com.suwiki.local.common.database.entity + + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.suwiki.common.model.timetable.TimetableDay +import kotlinx.serialization.Serializable + +@Entity +data class OpenLectureEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val name: String, + val type: String, + val major: String, + val grade: Int, + val professorName: String, + val cellList: List = emptyList() +) + +@Entity +@Serializable +data class CellEntity( + val location: String, + val day: TimetableDay, + val startPeriod: Int, + val endPeriod: Int +) diff --git a/local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt b/local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt index caad2083d..c955285fc 100644 --- a/local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt +++ b/local/openmajor/src/main/java/com/suwiki/local/openmajor/datasource/LocalOpenMajorDataSourceImpl.kt @@ -8,7 +8,7 @@ import com.suwiki.common.android.Dispatcher import com.suwiki.common.android.SuwikiDispatchers import com.suwiki.common.model.openmajor.OpenMajor import com.suwiki.data.openmajor.datasource.LocalOpenMajorDataSource -import com.suwiki.local.common.database.OpenMajorDatabase +import com.suwiki.local.common.database.database.OpenMajorDatabase import com.suwiki.local.common.datastore.di.NormalDataStore import com.suwiki.local.openmajor.converter.toEntity import com.suwiki.local.openmajor.converter.toModel diff --git a/local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt b/local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt new file mode 100644 index 000000000..a2ee96442 --- /dev/null +++ b/local/timetable/src/main/java/com.suwiki.local.timetable/converter/OpenLectureEntity.kt @@ -0,0 +1,40 @@ +package com.suwiki.local.timetable.converter + +import com.suwiki.common.model.timetable.Cell +import com.suwiki.common.model.timetable.OpenLecture +import com.suwiki.local.common.database.entity.CellEntity +import com.suwiki.local.common.database.entity.OpenLectureEntity + +fun OpenLectureEntity.toModel() = OpenLecture( + id = id, + name = name, + type = type, + major = major, + grade = grade, + professorName = professorName, + originalCellList = cellList.map { it.toModel() } +) + +fun OpenLecture.toEntity() = OpenLectureEntity( + id = id, + name = name, + type = type, + major = major, + grade = grade, + professorName = professorName, + cellList = originalCellList.map { it.toEntity() } +) + +fun Cell.toEntity() = CellEntity( + location = location, + day = day, + startPeriod = startPeriod, + endPeriod = endPeriod, +) + +fun CellEntity.toModel() = Cell( + location = location, + day = day, + startPeriod = startPeriod, + endPeriod = endPeriod, +) diff --git a/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt b/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt new file mode 100644 index 000000000..8686b99dd --- /dev/null +++ b/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalOpenLectureDatasourceImpl.kt @@ -0,0 +1,62 @@ +package com.suwiki.local.timetable.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import com.suwiki.common.android.Dispatcher +import com.suwiki.common.android.SuwikiDispatchers +import com.suwiki.common.model.timetable.OpenLecture +import com.suwiki.data.timetable.datasource.LocalOpenLectureDataSource +import com.suwiki.local.common.database.database.OpenLectureDatabase +import com.suwiki.local.common.datastore.di.NormalDataStore +import com.suwiki.local.timetable.converter.toEntity +import com.suwiki.local.timetable.converter.toModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class LocalOpenLectureDatasourceImpl @Inject constructor( + @NormalDataStore private val dataStore: DataStore, + @Dispatcher(SuwikiDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, + private val openLectureDatabase: OpenLectureDatabase, +) : LocalOpenLectureDataSource { + + companion object { + private val OPEN_LECTURE_LIST_VERSION = longPreferencesKey("[KEY] is open lecture list version") + } + + private val data: Flow + get() = dataStore.data + + override fun getOpenLectureListVersion(): Flow { + return data.map { it[OPEN_LECTURE_LIST_VERSION] } + } + + override suspend fun setOpenLectureListVersion(version: Long) { + dataStore.edit { it[OPEN_LECTURE_LIST_VERSION] = version } + } + + override fun getOpenLectureList( + lectureOrProfessorName: String?, + major: String?, + grade: Int?, + ): Flow> { + return openLectureDatabase + .openLectureDao() + .searchLectures( + lectureOrProfessorName = lectureOrProfessorName, + major = major, + grade = grade, + ) + .map { list -> list.map { it.toModel() } } + } + + override suspend fun updateAllLectures(lectures: List) = withContext(ioDispatcher) { + openLectureDatabase + .openLectureDao() + .updateAllLectures(lectures.map { it.toEntity() }) + } +} diff --git a/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt b/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt index 6fc144abc..289f272f1 100644 --- a/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt +++ b/local/timetable/src/main/java/com.suwiki.local.timetable/datasource/LocalTimetableDatasourceImpl.kt @@ -9,7 +9,7 @@ import com.suwiki.common.android.Dispatcher import com.suwiki.common.android.SuwikiDispatchers import com.suwiki.common.model.timetable.Timetable import com.suwiki.data.timetable.datasource.LocalTimetableDataSource -import com.suwiki.local.common.database.TimetableDatabase +import com.suwiki.local.common.database.database.TimetableDatabase import com.suwiki.local.common.datastore.di.NormalDataStore import com.suwiki.local.timetable.converter.toEntity import com.suwiki.local.timetable.converter.toModel diff --git a/local/timetable/src/main/java/com.suwiki.local.timetable/di/LocalDataSourceModule.kt b/local/timetable/src/main/java/com.suwiki.local.timetable/di/LocalDataSourceModule.kt index 22dc5bcbe..9a8477e77 100644 --- a/local/timetable/src/main/java/com.suwiki.local.timetable/di/LocalDataSourceModule.kt +++ b/local/timetable/src/main/java/com.suwiki.local.timetable/di/LocalDataSourceModule.kt @@ -1,6 +1,8 @@ package com.suwiki.local.timetable.di +import com.suwiki.data.timetable.datasource.LocalOpenLectureDataSource import com.suwiki.data.timetable.datasource.LocalTimetableDataSource +import com.suwiki.local.timetable.datasource.LocalOpenLectureDatasourceImpl import com.suwiki.local.timetable.datasource.LocalTimetableDatasourceImpl import dagger.Binds import dagger.Module @@ -17,4 +19,10 @@ abstract class LocalDataSourceModule { abstract fun bindLocalTimetableDataSource( localTimetableDatasourceImpl: LocalTimetableDatasourceImpl, ): LocalTimetableDataSource + + @Singleton + @Binds + abstract fun bindLocalOpenLectureDataSource( + localOpenLectureDatasourceImpl: LocalOpenLectureDatasourceImpl, + ): LocalOpenLectureDataSource } diff --git a/presentation/openmajor/build.gradle.kts b/presentation/openmajor/build.gradle.kts index a6648240f..0e82d25b2 100644 --- a/presentation/openmajor/build.gradle.kts +++ b/presentation/openmajor/build.gradle.kts @@ -9,4 +9,5 @@ android { dependencies { implementation(projects.domain.openmajor) + implementation(projects.domain.timetable) } diff --git a/presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt b/presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt index c537507a3..4a2471b6f 100644 --- a/presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt +++ b/presentation/openmajor/src/main/java/com/suwiki/presentation/openmajor/OpenMajorViewModel.kt @@ -8,6 +8,7 @@ import com.suwiki.domain.openmajor.usecase.GetBookmarkedOpenMajorListUseCase import com.suwiki.domain.openmajor.usecase.GetOpenMajorListUseCase import com.suwiki.domain.openmajor.usecase.RegisterBookmarkUseCase import com.suwiki.domain.openmajor.usecase.UnRegisterBookmarkUseCase +import com.suwiki.domain.timetable.repository.OpenLectureRepository import com.suwiki.presentation.openmajor.model.OpenMajorTap import com.suwiki.presentation.openmajor.model.toBookmarkedOpenMajorList import com.suwiki.presentation.openmajor.model.toOpenMajorList @@ -33,6 +34,7 @@ class OpenMajorViewModel @Inject constructor( private val getBookmarkedOpenMajorListUseCase: GetBookmarkedOpenMajorListUseCase, private val registerBookmarkUseCase: RegisterBookmarkUseCase, private val unRegisterBookmarkUseCase: UnRegisterBookmarkUseCase, + private val openLectureRepository: OpenLectureRepository, savedStateHandle: SavedStateHandle, ) : ContainerHost, ViewModel() { override val container: Container = container(OpenMajorState()) @@ -117,7 +119,8 @@ class OpenMajorViewModel @Inject constructor( private fun getOpenMajor() = intent { getOpenMajorListUseCase().onEach { allOpenMajorList.clear() - allOpenMajorList.addAll(it) + val firebaseOpenMajor = openLectureRepository.getOpenMajor() + allOpenMajorList.addAll((it + firebaseOpenMajor).distinct()) reduceOpenMajorList() }.catch { postSideEffect(OpenMajorSideEffect.HandleException(it)) diff --git a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt index 5b46d9bfe..32645aa60 100644 --- a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt +++ b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureContract.kt @@ -20,6 +20,7 @@ data class OpenLectureState( val showSelectCellColorBottomSheet: Boolean = false, val selectedTimetableCellColor: TimetableCellColor = TimetableCellColor.BROWN, val isLoading: Boolean = false, + val lastUpdatedDate: String? = null, ) fun List.toText(context: Context): String { diff --git a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt index 54fa9f1a8..8dc9c6af1 100644 --- a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt +++ b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureScreen.kt @@ -113,10 +113,6 @@ fun OpenLectureRoute( viewModel.initData() } - listState.OnBottomReached { - viewModel.getOpenLectureList(needClear = false) - } - OpenLectureScreen( uiState = uiState, listState = listState, @@ -218,10 +214,19 @@ fun OpenLectureScreen( onClickClearButton = onClickClearButton, onValueChange = onValueChangeSearch, ) + + Text( + modifier = Modifier + .padding(top = 10.dp, end = 20.dp) + .align(Alignment.End), + text = "최근 갱신일: ${uiState.lastUpdatedDate ?: "확인 중"}", + style = SuwikiTheme.typography.body7, + color = Gray95, + ) } }, ) { - if (uiState.openLectureList.isEmpty()) { + if (uiState.openLectureList.isEmpty() && uiState.isLoading.not()) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt index 547522cca..412bece57 100644 --- a/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt +++ b/presentation/timetable/src/main/java/com/suwiki/presentation/timetable/openlecture/OpenLectureViewModel.kt @@ -7,12 +7,16 @@ import com.suwiki.common.model.timetable.OpenLectureData import com.suwiki.common.model.timetable.TimetableCell import com.suwiki.common.model.timetable.TimetableCellColor import com.suwiki.common.model.timetable.TimetableDay +import com.suwiki.domain.timetable.repository.OpenLectureRepository import com.suwiki.domain.timetable.usecase.GetOpenLectureListUseCase import com.suwiki.domain.timetable.usecase.InsertTimetableCellUseCase +import com.suwiki.domain.timetable.usecase.UpdateOpenLectureIfNeedUseCase import com.suwiki.presentation.timetable.navigation.argument.toCellEditorArgument import com.suwiki.presentation.timetable.openlecture.model.SchoolLevel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.orbitmvi.orbit.Container @@ -28,8 +32,10 @@ import javax.inject.Inject @HiltViewModel class OpenLectureViewModel @Inject constructor( + private val updateOpenLectureIfNeedUseCase: UpdateOpenLectureIfNeedUseCase, private val getOpenLectureListUseCase: GetOpenLectureListUseCase, private val insertTimetableCellUseCase: InsertTimetableCellUseCase, + private val openLectureRepository: OpenLectureRepository, ) : ViewModel(), ContainerHost { private val mutex: Mutex = Mutex() @@ -41,13 +47,26 @@ class OpenLectureViewModel @Inject constructor( private val currentState get() = container.stateFlow.value - private var cursorId: Long = 0 - private var isLast: Boolean = false private var searchQuery: String = "" private var isFirstVisit: Boolean = true private var selectedOpenLecture: OpenLecture? = null + fun initData() = intent { + if (isFirstVisit) { + reduce { state.copy(isLoading = true) } + updateOpenLectureIfNeedUseCase() + val lastUpdated = openLectureRepository.getLastUpdatedDate() + reduce { + state.copy( + lastUpdatedDate = lastUpdated, + ) + } + getOpenLectureList() + isFirstVisit = false + } + } + fun navigateCellEditor(openLecture: OpenLecture = OpenLecture()) = intent { postSideEffect( OpenLectureSideEffect.NavigateCellEditor(openLecture.toCellEditorArgument()), @@ -112,16 +131,9 @@ class OpenLectureViewModel @Inject constructor( fun hideSelectColorBottomSheet() = intent { reduce { state.copy(showSelectCellColorBottomSheet = false) } } - fun initData() = intent { - if (isFirstVisit) { - getOpenLectureList(needClear = false) - isFirstVisit = false - } - } - fun searchOpenLecture(search: String) { searchQuery = search - getOpenLectureList(search = search, needClear = true) + getOpenLectureList(search = search) } @OptIn(OrbitExperimental::class) @@ -136,9 +148,7 @@ class OpenLectureViewModel @Inject constructor( ) } - getOpenLectureList( - needClear = true, - ) + getOpenLectureList() } fun updateSelectedOpenMajor(openMajor: String) = intent { @@ -149,66 +159,35 @@ class OpenLectureViewModel @Inject constructor( selectedOpenMajor = openMajor, ) } - - getOpenLectureList( - needClear = true, - ) + getOpenLectureList() } - fun getOpenLectureList( + 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) } } - private suspend fun SimpleSyntax.handleGetOpenLectureListSuccess( - currentList: List, - newData: OpenLectureData, - ) = reduce { - isLast = newData.isLast - cursorId = newData.content.lastOrNull()?.id ?: 0L - state.copy( - openLectureList = currentList - .plus(newData.content) - .distinctBy { it.id } - .toPersistentList(), - ) - } - fun showGradeBottomSheet() = intent { reduce { state.copy(showSchoolLevelBottomSheet = true) } } fun hideGradeBottomSheet() = intent { reduce { state.copy(showSchoolLevelBottomSheet = false) } } fun popBackStack() = intent { postSideEffect(OpenLectureSideEffect.PopBackStack) } diff --git a/remote/common/build.gradle.kts b/remote/common/build.gradle.kts index 6b0594144..aae932a16 100644 --- a/remote/common/build.gradle.kts +++ b/remote/common/build.gradle.kts @@ -10,7 +10,7 @@ android { buildTypes { getByName("debug") { - buildConfigField("String", "BASE_URL", "String.valueOf(\"http://54.180.72.97:8080\")") + buildConfigField("String", "BASE_URL", "\"https://api.suwiki.kr\"") } getByName("release") { @@ -30,5 +30,9 @@ dependencies { implementation(libs.okhttp.logging) androidTestImplementation(libs.junit4) + val bom = libs.firebase.bom + add("implementation", platform(bom)) + implementation(libs.bundles.firebase) + implementation(libs.timber) } diff --git a/remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt b/remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt new file mode 100644 index 000000000..33edc853d --- /dev/null +++ b/remote/common/src/main/java/com/suwiki/remote/common/di/FirebaseDatabaseModule.kt @@ -0,0 +1,19 @@ +package com.suwiki.remote.common.di + +import com.google.firebase.database.FirebaseDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FirebaseDatabaseModule { + + @Singleton + @Provides + fun provideFirebaseDatabase(): FirebaseDatabase { + return FirebaseDatabase.getInstance() + } +} diff --git a/remote/timetable/build.gradle.kts b/remote/timetable/build.gradle.kts index 763dad0d8..dce820294 100644 --- a/remote/timetable/build.gradle.kts +++ b/remote/timetable/build.gradle.kts @@ -13,4 +13,8 @@ dependencies { implementation(libs.retrofit.core) implementation(libs.kotlinx.serialization.json) + + val bom = libs.firebase.bom + add("implementation", platform(bom)) + implementation(libs.bundles.firebase) } diff --git a/remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt b/remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt index 8002084a6..453f25c47 100644 --- a/remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt +++ b/remote/timetable/src/main/java/com.suwiki.remote.timetable/datasource/RemoteOpenLectureDataSourceImpl.kt @@ -1,25 +1,56 @@ package com.suwiki.remote.timetable.datasource +import com.google.firebase.database.FirebaseDatabase +import com.suwiki.data.timetable.OpenLectureRaw import com.suwiki.data.timetable.datasource.RemoteOpenLectureDataSource -import com.suwiki.remote.timetable.api.OpenLectureApi -import com.suwiki.remote.timetable.response.toModel +import kotlinx.coroutines.tasks.await +import okhttp3.internal.toLongOrDefault import javax.inject.Inject class RemoteOpenLectureDataSourceImpl @Inject constructor( - private val openLectureApi: OpenLectureApi, + private val firebaseDatabase: FirebaseDatabase, ) : RemoteOpenLectureDataSource { - override suspend fun getOpenLectureList( - cursorId: Long, - size: Long, - keyword: String?, - major: String?, - grade: Int?, - ) = openLectureApi.getOpenLectureList( - cursorId = cursorId, - size = size, - keyword = keyword, - major = major, - grade = grade, - ).getOrThrow().toModel() + override suspend fun getOpenLectureListVersion(): Long = + firebaseDatabase + .getReference(DATABASE_OPEN_LECTURE_VERSION) + .get() + .await() + .value + .toString() + .toLongOrDefault(0) + + + override suspend fun getOpenLectureList(): List = 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, + ) + } + + + companion object { + private const val FIELD_MAJOR = "major" + private const val FIELD_GRADE = "grade" + private const val FIELD_CLASS_NAME = "name" + private const val FIELD_CLASSIFICATION = "divide" + private const val FIELD_PROFESSOR = "profe" + private const val FIELD_TIME = "time" + + private const val DEFAULT = "없음" + + private const val DATABASE_OPEN_LECTURE = "openLecture" + private const val DATABASE_OPEN_LECTURE_VERSION = "openLectureVersion" + } } diff --git a/schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json b/schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json new file mode 100644 index 000000000..36f49a284 --- /dev/null +++ b/schemas/com.suwiki.local.common.database.OpenLectureDatabase/1.json @@ -0,0 +1,88 @@ +{ + "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')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.OpenLectureDatabase/2.json b/schemas/com.suwiki.local.common.database.OpenLectureDatabase/2.json new file mode 100644 index 000000000..e484c6aee --- /dev/null +++ b/schemas/com.suwiki.local.common.database.OpenLectureDatabase/2.json @@ -0,0 +1,88 @@ +{ + "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')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.TimetableDatabase/1.json b/schemas/com.suwiki.local.common.database.TimetableDatabase/1.json index 0857765c2..573e8c31a 100644 --- a/schemas/com.suwiki.local.common.database.TimetableDatabase/1.json +++ b/schemas/com.suwiki.local.common.database.TimetableDatabase/1.json @@ -2,48 +2,78 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "80628cd5c243f45d44b3c48b16fc2277", + "identityHash": "fdfd5ad0cd87be70a7007f51d26b3ff7", "entities": [ { - "tableName": "TimeTableList", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`createTime` INTEGER NOT NULL, `year` TEXT NOT NULL, `semester` TEXT NOT NULL, `timeTableName` TEXT NOT NULL, `timeTableJsonData` TEXT NOT NULL, PRIMARY KEY(`createTime`))", + "tableName": "TimetableEntity", + "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": "createTime", - "columnName": "createTime", + "fieldPath": "number", + "columnName": "number", "affinity": "INTEGER", "notNull": true }, { - "fieldPath": "year", - "columnName": "year", + "fieldPath": "major", + "columnName": "major", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "semester", - "columnName": "semester", + "fieldPath": "grade", + "columnName": "grade", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "timeTableName", - "columnName": "timeTableName", + "fieldPath": "classNumber", + "columnName": "classNumber", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "timeTableJsonData", - "columnName": "timeTableJsonData", + "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": [ - "createTime" - ], - "autoGenerate": false + "number" + ] }, "indices": [], "foreignKeys": [] @@ -52,7 +82,7 @@ "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, '80628cd5c243f45d44b3c48b16fc2277')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fdfd5ad0cd87be70a7007f51d26b3ff7')" ] } -} \ No newline at end of file +} diff --git a/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json b/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json new file mode 100644 index 000000000..f51ce30bd --- /dev/null +++ b/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/1.json @@ -0,0 +1,70 @@ +{ + "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": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "major", + "columnName": "major", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "grade", + "columnName": "grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "professorName", + "columnName": "professorName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellList", + "columnName": "cellList", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'a9fed779395cf5534baf92fbef19edd1')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json b/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json new file mode 100644 index 000000000..e484c6aee --- /dev/null +++ b/schemas/com.suwiki.local.common.database.database.OpenLectureDatabase/2.json @@ -0,0 +1,88 @@ +{ + "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')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json b/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json new file mode 100644 index 000000000..4c59795ba --- /dev/null +++ b/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/1.json @@ -0,0 +1,40 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "a7aa694667974b6b111d2d7755530138", + "entities": [ + { + "tableName": "OpenMajorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'a7aa694667974b6b111d2d7755530138')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json b/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json new file mode 100644 index 000000000..c76915019 --- /dev/null +++ b/schemas/com.suwiki.local.common.database.database.OpenMajorDatabase/2.json @@ -0,0 +1,40 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "a7aa694667974b6b111d2d7755530138", + "entities": [ + { + "tableName": "OpenMajorEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'a7aa694667974b6b111d2d7755530138')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json b/schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json new file mode 100644 index 000000000..f176d8e47 --- /dev/null +++ b/schemas/com.suwiki.local.common.database.database.TimetableDatabase/1.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "80628cd5c243f45d44b3c48b16fc2277", + "entities": [ + { + "tableName": "TimeTableList", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`createTime` INTEGER NOT NULL, `year` TEXT NOT NULL, `semester` TEXT NOT NULL, `timeTableName` TEXT NOT NULL, `timeTableJsonData` TEXT NOT NULL, PRIMARY KEY(`createTime`))", + "fields": [ + { + "fieldPath": "createTime", + "columnName": "createTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "semester", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeTableName", + "columnName": "timeTableName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeTableJsonData", + "columnName": "timeTableJsonData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "createTime" + ], + "autoGenerate": false + }, + "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, '80628cd5c243f45d44b3c48b16fc2277')" + ] + } +} diff --git a/schemas/com.suwiki.local.common.database.TimetableDatabase/2.json b/schemas/com.suwiki.local.common.database.database.TimetableDatabase/2.json similarity index 99% rename from schemas/com.suwiki.local.common.database.TimetableDatabase/2.json rename to schemas/com.suwiki.local.common.database.database.TimetableDatabase/2.json index ef8b7650d..4824b90f7 100644 --- a/schemas/com.suwiki.local.common.database.TimetableDatabase/2.json +++ b/schemas/com.suwiki.local.common.database.database.TimetableDatabase/2.json @@ -55,4 +55,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd3ae462e5e5245e675c522c8e1ff9800')" ] } -} \ No newline at end of file +}