v0.2.8: Site & Keyword Reordering Support & Slim Bottom Bar Margins

This commit is contained in:
2026-08-20 23:53:26 +00:00
parent 7ae5f713d6
commit 5c272a76e7
@@ -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.HotDealDao
import com.hotdeal.alarm.data.local.db.dao.KeywordDao 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.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.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.SiteType import com.hotdeal.alarm.domain.model.SiteType
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.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -24,28 +24,34 @@ class MainViewModel @Inject constructor(
private val workerScheduler: WorkerScheduler, private val workerScheduler: WorkerScheduler,
private val appSettings: AppSettings private val appSettings: AppSettings
) : ViewModel() { ) : ViewModel() {
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()
// 폴링 주기 (저장된 값 즉시 반영) // 폴링 주기
val pollingInterval: StateFlow<Int> = appSettings.pollingInterval val pollingInterval: StateFlow<Int> = appSettings.pollingInterval
.stateIn(viewModelScope, SharingStarted.Eagerly, 2) .stateIn(viewModelScope, SharingStarted.Eagerly, 2)
// 사이트 표시 순서
val siteOrder: StateFlow<List<SiteType>> = appSettings.siteOrder
.stateIn(viewModelScope, SharingStarted.Eagerly, SiteType.entries)
private val _toastEvent = MutableSharedFlow<String>()
val toastEvent: SharedFlow<String> = _toastEvent.asSharedFlow()
init { init {
initializeApp() initializeApp()
} }
private fun initializeApp() { private fun initializeApp() {
viewModelScope.launch { viewModelScope.launch {
initializeDefaultSiteConfigs() initializeDefaultSiteConfigs()
loadState() loadState()
// 저장된 폴 주기로 시작
val savedInterval = appSettings.pollingInterval.first() val savedInterval = appSettings.pollingInterval.first()
startPolling(savedInterval.toLong()) startPolling(savedInterval.toLong())
} }
} }
private suspend fun initializeDefaultSiteConfigs() { private suspend fun initializeDefaultSiteConfigs() {
val existingConfigs = siteConfigDao.getAllConfigs() val existingConfigs = siteConfigDao.getAllConfigs()
val existingKeys = existingConfigs.map { it.siteBoardKey }.toSet() val existingKeys = existingConfigs.map { it.siteBoardKey }.toSet()
@@ -67,17 +73,27 @@ class MainViewModel @Inject constructor(
siteConfigDao.insertConfigs(missingConfigs) siteConfigDao.insertConfigs(missingConfigs)
} }
} }
private suspend fun loadState() { private suspend fun loadState() {
combine( combine(
hotDealDao.observeAllDeals(), hotDealDao.observeAllDeals(),
siteConfigDao.observeAllConfigs(), siteConfigDao.observeAllConfigs(),
keywordDao.observeAllKeywords() keywordDao.observeAllKeywords(),
) { deals, configs, keywords -> 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( MainUiState.Success(
deals = deals.map { it.toDomain() }, deals = deals.map { it.toDomain() },
siteConfigs = configs.map { it.toDomain() }, siteConfigs = configs.map { it.toDomain() },
keywords = keywords.map { it.toDomain() } keywords = sortedKeywords
) )
}.catch { e -> }.catch { e ->
_uiState.value = MainUiState.Error(e.message ?: "Unknown error") _uiState.value = MainUiState.Error(e.message ?: "Unknown error")
@@ -85,87 +101,108 @@ class MainViewModel @Inject constructor(
_uiState.value = state _uiState.value = state
} }
} }
fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) { fun toggleSiteConfig(siteBoardKey: String, enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
siteConfigDao.updateEnabled(siteBoardKey, enabled) 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) { fun addKeyword(keyword: String) {
if (keyword.isBlank()) return if (keyword.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
keywordDao.insertKeyword( val newId = keywordDao.insertKeyword(
com.hotdeal.alarm.data.local.db.entity.KeywordEntity( KeywordEntity(
keyword = keyword.trim(), keyword = keyword.trim(),
isEnabled = true isEnabled = true
) )
) )
// 키워드 순서 맨 앞에 추가
val currentOrder = appSettings.keywordOrder.first().toMutableList()
currentOrder.add(0, newId)
appSettings.setKeywordOrder(currentOrder)
} }
} }
fun deleteKeyword(id: Long) { fun deleteKeyword(id: Long) {
viewModelScope.launch { viewModelScope.launch {
keywordDao.deleteKeywordById(id) 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 toggleKeyword(id: Long, enabled: Boolean) {
* 즐겨찾기 토글 viewModelScope.launch {
*/ keywordDao.updateEnabled(id, enabled)
fun toggleFavorite(dealId: String) { }
viewModelScope.launch { }
hotDealDao.toggleFavorite(dealId)
} 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() { fun refresh() {
workerScheduler.executeOnce() workerScheduler.executeOnce()
} }
/**
* 폴 시작 (주기 저장)
*/
fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) { fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) {
viewModelScope.launch { viewModelScope.launch {
appSettings.setPollingInterval(intervalMinutes.toInt()) appSettings.setPollingInterval(intervalMinutes.toInt())
workerScheduler.schedulePeriodicPolling(intervalMinutes) workerScheduler.schedulePeriodicPolling(intervalMinutes)
} }
} }
fun stopPolling() { fun stopPolling() {
workerScheduler.cancelPolling() workerScheduler.cancelPolling()
} }
// 데이터 파싱 핫딜 데이터 전체 삭제 및 사용자 피드백 트리거
private val _toastEvent = MutableSharedFlow<String>(extraBufferCapacity = 1)
val toastEvent = _toastEvent.asSharedFlow()
fun deleteAllParsedData() { fun deleteAllParsedData() {
viewModelScope.launch { viewModelScope.launch {
hotDealDao.deleteAllDeals() try {
_toastEvent.emit("파싱 데이터가 삭제되었습니다") hotDealDao.deleteAllDeals()
_toastEvent.emit("모든 수집 데이터가 삭제되었습니다")
} catch (e: Exception) {
_toastEvent.emit("데이터 삭제 중 오류가 발생했습니다: ${e.message}")
}
} }
} }
} }
sealed class MainUiState { sealed class MainUiState {
data object Loading : MainUiState() object Loading : MainUiState()
data class Success( data class Success(
val deals: List<com.hotdeal.alarm.domain.model.HotDeal>, val deals: List<com.hotdeal.alarm.domain.model.HotDeal>,
val siteConfigs: List<com.hotdeal.alarm.domain.model.SiteConfig>, val siteConfigs: List<com.hotdeal.alarm.domain.model.SiteConfig>,