v0.3.5: update MainViewModel.kt

This commit is contained in:
sanjeok77 committed 2026-08-25 02:11:08 +00:00
1 parent af33f0d02b
commit 9785f55b81
1 file changed
+35 -13
@@ -10,6 +10,7 @@ import com.hotdeal.alarm.data.local.db.entity.SiteConfigEntity
import com.hotdeal.alarm.data.local.preferences.AppSettings import com.hotdeal.alarm.data.local.preferences.AppSettings
import com.hotdeal.alarm.domain.model.Keyword import com.hotdeal.alarm.domain.model.Keyword
import com.hotdeal.alarm.domain.model.SiteType import com.hotdeal.alarm.domain.model.SiteType
import com.hotdeal.alarm.domain.usecase.HotDealScraperManager
import com.hotdeal.alarm.worker.WorkerScheduler import com.hotdeal.alarm.worker.WorkerScheduler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -21,6 +22,7 @@ class MainViewModel @Inject constructor(
private val hotDealDao: HotDealDao, private val hotDealDao: HotDealDao,
private val siteConfigDao: SiteConfigDao, private val siteConfigDao: SiteConfigDao,
private val keywordDao: KeywordDao, private val keywordDao: KeywordDao,
private val scraperManager: HotDealScraperManager,
private val workerScheduler: WorkerScheduler, private val workerScheduler: WorkerScheduler,
private val appSettings: AppSettings private val appSettings: AppSettings
) : ViewModel() { ) : ViewModel() {
@@ -28,6 +30,10 @@ class MainViewModel @Inject constructor(
private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading) private val _uiState = MutableStateFlow<MainUiState>(MainUiState.Loading)
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow() val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
// 새로고침 진행 상태
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
// 폴링 주기 // 폴링 주기
val pollingInterval: StateFlow<Int> = appSettings.pollingInterval val pollingInterval: StateFlow<Int> = appSettings.pollingInterval
.stateIn(viewModelScope, SharingStarted.Eagerly, 2) .stateIn(viewModelScope, SharingStarted.Eagerly, 2)
@@ -44,18 +50,19 @@ class MainViewModel @Inject constructor(
} }
private fun initializeApp() { private fun initializeApp() {
// 1. 사이트 설정 초기화 후 DB Flow 구독 (별도 코루틴 — collect는 영원히 suspend) // 1. 사이트 설정 초기화 후 DB Flow 구독 (별도 코루틴 — collect는 지속 실행)
viewModelScope.launch { viewModelScope.launch {
initializeDefaultSiteConfigs() initializeDefaultSiteConfigs()
loadState() // combine().collect — 이 코루틴은 영원히 살아있음 loadState()
} }
// 2. 백그라운드 폴링 등록 + 즉시 1회 스크래핑 (별도 코루틴) // 2. 백그라운드 폴링 등록 + 앱 진입 즉시 직접 1회 스크래핑 실행
viewModelScope.launch { viewModelScope.launch {
val savedInterval = appSettings.pollingInterval.first() val savedInterval = appSettings.pollingInterval.first()
workerScheduler.schedulePeriodicPolling(savedInterval.toLong()) workerScheduler.schedulePeriodicPolling(savedInterval.toLong())
// 앱 시작 시 즉시 1회 실행하여 최신 데이터 갱신
workerScheduler.executeOnce() // 지연 없는 직접 스크래핑 실행 -> Room Flow를 통해 메인화면에 즉시 반영됨!
refresh()
} }
} }
@@ -70,7 +77,7 @@ class MainViewModel @Inject constructor(
siteName = site.name, siteName = site.name,
boardName = board.id, boardName = board.id,
displayName = "${site.displayName} - ${board.displayName}", displayName = "${site.displayName} - ${board.displayName}",
isEnabled = true // 기본값: 활성화 (설치 즉시 수집 작동) isEnabled = true // 기본 활성화 (설치 즉시 수집 가동)
) )
} }
} }
@@ -88,7 +95,6 @@ class MainViewModel @Inject constructor(
keywordDao.observeAllKeywords(), keywordDao.observeAllKeywords(),
appSettings.keywordOrder appSettings.keywordOrder
) { deals, configs, keywords, keywordOrder -> ) { deals, configs, keywords, keywordOrder ->
// 사용자가 설정한 키워드 순서에 따라 정렬
val domainKeywords = keywords.map { it.toDomain() } val domainKeywords = keywords.map { it.toDomain() }
val sortedKeywords = if (keywordOrder.isEmpty()) { val sortedKeywords = if (keywordOrder.isEmpty()) {
domainKeywords domainKeywords
@@ -112,12 +118,14 @@ class MainViewModel @Inject constructor(
fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) { fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
siteConfigDao.updateEnabled(siteBoardKey, enabled) siteConfigDao.updateEnabled(siteBoardKey, enabled)
// 사이트 활성화 변경 시 즉시 새 핫딜 갱신
if (enabled) {
refresh()
}
} }
} }
fun reorderSites(fromIndex: Int, toIndex: Int) { fun reorderSites(fromIndex: Int, toIndex: Int) {
// siteOrder는 appSettings.siteOrder에서 stateIn으로 가져오므로
// appSettings를 즉시 저장하고 Flow가 업데이트되도록 함
val current = siteOrder.value.toMutableList() val current = siteOrder.value.toMutableList()
if (fromIndex in current.indices && toIndex in current.indices) { if (fromIndex in current.indices && toIndex in current.indices) {
val item = current.removeAt(fromIndex) val item = current.removeAt(fromIndex)
@@ -140,6 +148,8 @@ class MainViewModel @Inject constructor(
val currentOrder = appSettings.keywordOrder.first().toMutableList() val currentOrder = appSettings.keywordOrder.first().toMutableList()
currentOrder.add(0, newId) currentOrder.add(0, newId)
appSettings.setKeywordOrder(currentOrder) appSettings.setKeywordOrder(currentOrder)
// 키워드 등록 후 매칭 검사를 위해 즉시 갱신
refresh()
} }
} }
@@ -155,19 +165,19 @@ class MainViewModel @Inject constructor(
fun toggleKeyword(id: Long, enabled: Boolean) { fun toggleKeyword(id: Long, enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
keywordDao.updateEnabled(id, enabled) keywordDao.updateEnabled(id, enabled)
if (enabled) {
refresh()
}
} }
} }
fun reorderKeywords(fromIndex: Int, toIndex: Int) { fun reorderKeywords(fromIndex: Int, toIndex: Int) {
// uiState를 동기적으로 즉시 업데이트하여 빠른 드래그 시 stale 데이터 방지
val state = _uiState.value as? MainUiState.Success ?: return val state = _uiState.value as? MainUiState.Success ?: return
val current = state.keywords.toMutableList() val current = state.keywords.toMutableList()
if (fromIndex in current.indices && toIndex in current.indices) { if (fromIndex in current.indices && toIndex in current.indices) {
val item = current.removeAt(fromIndex) val item = current.removeAt(fromIndex)
current.add(toIndex, item) current.add(toIndex, item)
// UI 즉시 갱신
_uiState.value = state.copy(keywords = current) _uiState.value = state.copy(keywords = current)
// DataStore 비동기 영속화
viewModelScope.launch { viewModelScope.launch {
appSettings.setKeywordOrder(current.map { it.id }) appSettings.setKeywordOrder(current.map { it.id })
} }
@@ -186,8 +196,20 @@ class MainViewModel @Inject constructor(
} }
} }
/**
* 직접 즉시 스크래핑 실행 (WorkManager 큐 대기 지연 없이 UI 및 DB 즉시 갱신)
*/
fun refresh() { fun refresh() {
workerScheduler.executeOnce() viewModelScope.launch {
_isRefreshing.value = true
try {
scraperManager.fetchAndProcessDeals()
} catch (e: Exception) {
// 에러 발생 시 무시 (UI는 DB Flow로 유지)
} finally {
_isRefreshing.value = false
}
}
} }
fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) { fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) {