v0.3.5: update HotDealPollingWorker.kt
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user