v0.3.5: update HotDealPollingWorker.kt

This commit is contained in:
sanjeok77 committed 2026-08-25 02:11:05 +00:00
1 parent 986e50088b
commit 88a8d6095e
1 file changed
+10 -158
@@ -6,33 +6,20 @@ import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.hotdeal.alarm.data.local.db.entity.HotDealEntity
import com.hotdeal.alarm.data.local.db.entity.KeywordEntity
import com.hotdeal.alarm.data.local.db.entity.SiteConfigEntity
import com.hotdeal.alarm.data.remote.scraper.ScraperFactory
import com.hotdeal.alarm.domain.model.HotDeal
import com.hotdeal.alarm.domain.usecase.HotDealScraperManager
import com.hotdeal.alarm.service.NotificationService
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
/**
* 핫딜 폴� Worker
*
* 키워드 매칭된 핫딜만 알림 발송
* 핫딜 폴링 Worker (WorkManager 전용)
*/
@HiltWorker
class HotDealPollingWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted workerParams: WorkerParameters,
private val scraperFactory: ScraperFactory,
private val notificationService: NotificationService,
private val dealDao: com.hotdeal.alarm.data.local.db.dao.HotDealDao,
private val siteConfigDao: com.hotdeal.alarm.data.local.db.dao.SiteConfigDao,
private val keywordDao: com.hotdeal.alarm.data.local.db.dao.KeywordDao
private val scraperManager: HotDealScraperManager,
private val notificationService: NotificationService
) : CoroutineWorker(context, workerParams) {
companion object {
@@ -40,153 +27,18 @@ class HotDealPollingWorker @AssistedInject constructor(
const val TAG = "HotDealPolling"
}
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "===== 핫딜 폴� 시작 =====")
// 1. 활성화된 사이트 설정 조회
val enabledConfigs = siteConfigDao.getEnabledConfigs()
Log.d(TAG, "활성화된 사이트 설정: ${enabledConfigs.size}개")
if (enabledConfigs.isEmpty()) {
Log.w(TAG, "활성화된 사이트 설정이 없습니다.")
return@withContext Result.success()
}
// 2. 활성화된 키워드 조회
val keywords = keywordDao.getEnabledKeywords()
Log.d(TAG, "활성화된 키워드: ${keywords.size}개")
// 3. 각 사이트/게시판 스크래핑 (병렬)
Log.d(TAG, "스크래핑 시작...")
val allDeals = enabledConfigs.map { config ->
async {
scrapeSite(config)
}
}.awaitAll().flatten()
Log.d(TAG, "스크래핑 완료: 총 ${allDeals.size}개 핫딜")
if (allDeals.isEmpty()) {
Log.w(TAG, "스크래핑된 핫딜이 없습니다.")
return@withContext Result.success()
}
// 4. 새로운 핫딜 필터링
val newDeals = filterNewDeals(allDeals)
Log.d(TAG, "새로운 핫딜: ${newDeals.size}개")
if (newDeals.isEmpty()) {
Log.d(TAG, "새로운 핫딜이 없습니다.")
return@withContext Result.success()
}
// 5. 키워드 매칭 (키워드가 있을 때만)
val keywordMatchedDeals = if (keywords.isNotEmpty()) {
filterByKeywords(newDeals, keywords).also {
Log.d(TAG, "키워드 매칭 핫딜: ${it.size}개")
}
} else {
Log.d(TAG, "키워드가 없어 매칭 체크를 건�뜁니다.")
emptyList()
}
// 6. DB 저장
var insertedCount = 0
newDeals.forEach { deal ->
try {
val entity = HotDealEntity.fromDomain(deal)
dealDao.insertDeal(entity)
insertedCount++
} catch (e: Exception) {
Log.w(TAG, "핫딜 저장 실패 (무시됨): ${deal.id}")
}
}
Log.d(TAG, "DB 저장 완료: $insertedCount/${newDeals.size}개 저장됨")
// 7. 키워드 매칭 알림 발송 (키워드 매칭된 것만!)
if (keywordMatchedDeals.isNotEmpty()) {
Log.d(TAG, "키워드 매칭 알림 발송: ${keywordMatchedDeals.size}개")
notificationService.showKeywordMatchNotification(keywordMatchedDeals)
dealDao.markAsKeywordMatch(keywordMatchedDeals.map { it.id })
} else {
Log.d(TAG, "키워드 매칭된 핫딜이 없어 알림을 발송하지 않습니다.")
}
// 8. 알림 발송 상태 업데이트
dealDao.markAsNotified(newDeals.map { it.id })
// 9. 인기글 상태 업데이트 (신규+기존 딜 모두 HOT 마킹)
val popularDealIds = allDeals.filter { it.isPopular }.map { it.id }
if (popularDealIds.isNotEmpty()) {
dealDao.markAsPopular(popularDealIds)
Log.d(TAG, "인기글 마킹: ${popularDealIds.size}개")
}
// 10. 오래된 데이터 정리 (3일 이상 - 배터리/데이터 절약)
val threshold = System.currentTimeMillis() - (3 * 24 * 60 * 60 * 1000L)
dealDao.deleteOldDeals(threshold)
Log.d(TAG, "===== 핫딜 폴 완료 =====")
override suspend fun doWork(): Result {
Log.d(TAG, "WorkManager HotDealPollingWorker 실행됨")
return try {
scraperManager.fetchAndProcessDeals()
Result.success()
} catch (e: Exception) {
Log.e(TAG, "폴� 오류: ${e.message}", e)
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
Log.e(TAG, "Worker 실행 실패: ${e.message}", e)
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return notificationService.createForegroundInfo()
}
private suspend fun scrapeSite(config: SiteConfigEntity): List<HotDeal> {
val scraper = scraperFactory.getScraper(config.siteName)
if (scraper == null) {
Log.w(TAG, "스크래퍼를 찾을 수 없음: ${config.siteName}")
return emptyList()
}
Log.d(TAG, "스크래핑: ${config.siteName}/${config.boardName}")
val result = scraper.scrape(config.boardName)
result.fold(
onSuccess = { deals ->
Log.d(TAG, " ${config.siteName}: ${deals.size}개 핫딜 파싱됨")
try {
siteConfigDao.updateLastScrapedAt(config.siteBoardKey, System.currentTimeMillis())
} catch (e: Exception) {
Log.w(TAG, "lastScrapedAt 업데이트 실패: ${config.siteBoardKey}")
}
},
onFailure = { error ->
Log.e(TAG, " ${config.siteName} 스크래핑 실패: ${error.message}")
}
)
return result.getOrElse { emptyList() }
}
private suspend fun filterNewDeals(deals: List<HotDeal>): List<HotDeal> {
return deals.filter { deal ->
val existingByUrl = dealDao.getDealByUrl(deal.url)
if (existingByUrl != null) return@filter false
val existingById = dealDao.getDealById(deal.id)
if (existingById != null) return@filter false
true
}
}
private fun filterByKeywords(deals: List<HotDeal>, keywords: List<KeywordEntity>): List<HotDeal> {
return deals.filter { deal ->
keywords.any { keyword ->
deal.title.contains(keyword.keyword, ignoreCase = true)
}
}
}
}