Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/timetable/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ android {

dependencies {
implementation(projects.domain.timetable)
testImplementation(libs.junit4)
}
Original file line number Diff line number Diff line change
@@ -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 = "",
)
Original file line number Diff line number Diff line change
@@ -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<Cell> {
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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Q. 원래 이런 로직이 있었나요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

0f80dbf#diff-499cc6459302a199d0e9dd8e67ce0caa373dce6e8f16563d39fdc154517f0dd4

참고 커밋에 해당 로직이 있어서 추가 했습니다

},
startPeriod = start,
endPeriod = end
)
}
}.toList()
} ?: emptyList()
}

return result
}

private fun List<Int>.groupConsecutive(): List<Pair<Int, Int>> {
if (isEmpty()) return emptyList()
val result = mutableListOf<Pair<Int, Int>>()
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
}
}
Original file line number Diff line number Diff line change
@@ -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<Long?>

suspend fun setOpenLectureListVersion(version: Long)

fun getOpenLectureList(
lectureOrProfessorName: String? = null,
major: String? = null,
grade: Int? = null
): Flow<List<OpenLecture>>

suspend fun updateAllLectures(lectures: List<OpenLecture>)
}
Original file line number Diff line number Diff line change
@@ -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<OpenLectureRaw>
}
Original file line number Diff line number Diff line change
@@ -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<List<OpenLecture>> {
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
}
Comment on lines +36 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

네트워크 예외 처리 누락
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.

Suggested change
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
}


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())
}
Comment on lines +42 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

원격 버전·목록 2중 호출로 인한 오버헤드
async 블록을 두 번 사용해 동일 원격 경로를 두 번 조회하면 지연 시간이 늘고, 과금이 있는 Firebase 사용 시 비용이 증가할 수 있습니다. 버전과 목록을 한 번의 호출로 가져오거나, 목록 응답에 포함된 타임스탬프를 재사용하는 구조로 리팩터링을 고려해 주세요.


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<String> {
return localOpenLectureDataSource.getOpenLectureList().map { list -> list.map { it.major } }.firstOrNull() ?: emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -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<List<OpenLecture>>

suspend fun checkNeedUpdate(): Boolean

suspend fun updateAllLectures()

suspend fun getLastUpdatedDate(): String?

suspend fun getOpenMajor(): List<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,18 @@ import javax.inject.Inject
class GetOpenLectureListUseCase @Inject constructor(
private val openLectureRepository: OpenLectureRepository,
) {
suspend operator fun invoke(param: Param): Result<OpenLectureData> = 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?,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit> = runCatchingIgnoreCancelled {
if(openLectureRepository.checkNeedUpdate().not()) return@runCatchingIgnoreCancelled
openLectureRepository.updateAllLectures()
}
}
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CellEntity>): String {
return json.encodeToString(value)
}

@TypeConverter
fun toCellList(value: String): List<CellEntity> {
return json.decodeFromString(value)
}
}
Original file line number Diff line number Diff line change
@@ -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<List<OpenLectureEntity>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLectures(lectures: List<OpenLectureEntity>)

@Query("DELETE FROM OpenLectureEntity")
suspend fun deleteAllLectures()

@Transaction
suspend fun updateAllLectures(lectures: List<OpenLectureEntity>) {
deleteAllLectures()
insertLectures(lectures)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading