From 5c272a76e7de4fce4ed811a807552909f7c0058c Mon Sep 17 00:00:00 2001 From: sanjeok77 <1+sanjeok77@noreply.localhost> Date: Thu, 20 Aug 2026 23:53:26 +0000 Subject: [PATCH] v0.2.8: Site & Keyword Reordering Support & Slim Bottom Bar Margins --- .../alarm/presentation/main/MainViewModel.kt | 143 +++++++++++------- 1 file changed, 90 insertions(+), 53 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 186eb7d..741f03a 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 @@ -5,15 +5,15 @@ import androidx.lifecycle.viewModelScope import com.hotdeal.alarm.data.local.db.dao.HotDealDao import com.hotdeal.alarm.data.local.db.dao.KeywordDao import com.hotdeal.alarm.data.local.db.dao.SiteConfigDao +import com.hotdeal.alarm.data.local.db.entity.KeywordEntity 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.worker.WorkerScheduler import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject @HiltViewModel @@ -24,28 +24,34 @@ class MainViewModel @Inject constructor( private val workerScheduler: WorkerScheduler, private val appSettings: AppSettings ) : ViewModel() { - + private val _uiState = MutableStateFlow(MainUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() - - // 폴링 주기 (저장된 값 즉시 반영) + + // 폴링 주기 val pollingInterval: StateFlow = appSettings.pollingInterval .stateIn(viewModelScope, SharingStarted.Eagerly, 2) - + + // 사이트 표시 순서 + val siteOrder: StateFlow> = appSettings.siteOrder + .stateIn(viewModelScope, SharingStarted.Eagerly, SiteType.entries) + + private val _toastEvent = MutableSharedFlow() + val toastEvent: SharedFlow = _toastEvent.asSharedFlow() + init { initializeApp() } - + private fun initializeApp() { viewModelScope.launch { initializeDefaultSiteConfigs() loadState() - // 저장된 폴� 주기로 시작 val savedInterval = appSettings.pollingInterval.first() startPolling(savedInterval.toLong()) } } - + private suspend fun initializeDefaultSiteConfigs() { val existingConfigs = siteConfigDao.getAllConfigs() val existingKeys = existingConfigs.map { it.siteBoardKey }.toSet() @@ -67,17 +73,27 @@ class MainViewModel @Inject constructor( siteConfigDao.insertConfigs(missingConfigs) } } - + private suspend fun loadState() { combine( hotDealDao.observeAllDeals(), siteConfigDao.observeAllConfigs(), - keywordDao.observeAllKeywords() - ) { deals, configs, keywords -> + keywordDao.observeAllKeywords(), + appSettings.keywordOrder + ) { deals, configs, keywords, keywordOrder -> + // 사용자가 설정한 키워드 순서에 따라 정렬 + val domainKeywords = keywords.map { it.toDomain() } + val sortedKeywords = if (keywordOrder.isEmpty()) { + domainKeywords + } else { + val orderMap = keywordOrder.withIndex().associate { it.value to it.index } + domainKeywords.sortedBy { orderMap[it.id] ?: Int.MAX_VALUE } + } + MainUiState.Success( deals = deals.map { it.toDomain() }, siteConfigs = configs.map { it.toDomain() }, - keywords = keywords.map { it.toDomain() } + keywords = sortedKeywords ) }.catch { e -> _uiState.value = MainUiState.Error(e.message ?: "Unknown error") @@ -85,87 +101,108 @@ class MainViewModel @Inject constructor( _uiState.value = state } } - + fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) { viewModelScope.launch { siteConfigDao.updateEnabled(siteBoardKey, enabled) } } - + + fun reorderSites(fromIndex: Int, toIndex: Int) { + viewModelScope.launch { + val current = siteOrder.value.toMutableList() + if (fromIndex in current.indices && toIndex in current.indices) { + val item = current.removeAt(fromIndex) + current.add(toIndex, item) + appSettings.setSiteOrder(current) + } + } + } + fun addKeyword(keyword: String) { if (keyword.isBlank()) return viewModelScope.launch { - keywordDao.insertKeyword( - com.hotdeal.alarm.data.local.db.entity.KeywordEntity( + val newId = keywordDao.insertKeyword( + KeywordEntity( keyword = keyword.trim(), isEnabled = true ) ) + // 키워드 순서 맨 앞에 추가 + val currentOrder = appSettings.keywordOrder.first().toMutableList() + currentOrder.add(0, newId) + appSettings.setKeywordOrder(currentOrder) } } - + fun deleteKeyword(id: Long) { viewModelScope.launch { keywordDao.deleteKeywordById(id) + val currentOrder = appSettings.keywordOrder.first().toMutableList() + currentOrder.remove(id) + appSettings.setKeywordOrder(currentOrder) } } - - fun toggleKeyword(id: Long, enabled: Boolean) { - viewModelScope.launch { - keywordDao.updateEnabled(id, enabled) - } - } - /** - * 즐겨찾기 토글 - */ - fun toggleFavorite(dealId: String) { - viewModelScope.launch { - hotDealDao.toggleFavorite(dealId) - } - } + fun toggleKeyword(id: Long, enabled: Boolean) { + viewModelScope.launch { + keywordDao.updateEnabled(id, enabled) + } + } + + fun reorderKeywords(fromIndex: Int, toIndex: Int) { + val state = uiState.value as? MainUiState.Success ?: return + viewModelScope.launch { + val current = state.keywords.toMutableList() + if (fromIndex in current.indices && toIndex in current.indices) { + val item = current.removeAt(fromIndex) + current.add(toIndex, item) + appSettings.setKeywordOrder(current.map { it.id }) + } + } + } + + fun toggleFavorite(dealId: String) { + viewModelScope.launch { + hotDealDao.toggleFavorite(dealId) + } + } + + fun setFavorite(dealId: String, isFavorite: Boolean) { + viewModelScope.launch { + hotDealDao.setFavorite(dealId, isFavorite) + } + } - /** - * 즐겨찾기 설정 - */ - fun setFavorite(dealId: String, isFavorite: Boolean) { - viewModelScope.launch { - hotDealDao.setFavorite(dealId, isFavorite) - } - } - fun refresh() { workerScheduler.executeOnce() } - - /** - * 폴� 시작 (주기 저장) - */ + fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) { viewModelScope.launch { appSettings.setPollingInterval(intervalMinutes.toInt()) workerScheduler.schedulePeriodicPolling(intervalMinutes) } } - + fun stopPolling() { workerScheduler.cancelPolling() } - // 데이터 파싱 핫딜 데이터 전체 삭제 및 사용자 피드백 트리거 - private val _toastEvent = MutableSharedFlow(extraBufferCapacity = 1) - val toastEvent = _toastEvent.asSharedFlow() - fun deleteAllParsedData() { viewModelScope.launch { - hotDealDao.deleteAllDeals() - _toastEvent.emit("파싱 데이터가 삭제되었습니다") + try { + hotDealDao.deleteAllDeals() + _toastEvent.emit("모든 수집 데이터가 삭제되었습니다") + } catch (e: Exception) { + _toastEvent.emit("데이터 삭제 중 오류가 발생했습니다: ${e.message}") + } } } } sealed class MainUiState { - data object Loading : MainUiState() + object Loading : MainUiState() data class Success( val deals: List, val siteConfigs: List,