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.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>(MainUiState.Loading)
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
// 폴링 주기 (저장된 값 즉시 반영)
// 폴링 주기
val pollingInterval: StateFlow<Int> = appSettings.pollingInterval
.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 {
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<String>(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<com.hotdeal.alarm.domain.model.HotDeal>,
val siteConfigs: List<com.hotdeal.alarm.domain.model.SiteConfig>,