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