diff --git a/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt b/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt index 21e241d..1e501ba 100644 --- a/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt +++ b/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt @@ -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 { - 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): List { - 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, keywords: List): List { - return deals.filter { deal -> - keywords.any { keyword -> - deal.title.contains(keyword.keyword, ignoreCase = true) - } - } - } }