v0.3.5: add HotDealScraperManager.kt
This commit is contained in:
1 parent
9e0a9d7476
commit
34ba7b5a0f
1 file changed
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package com.hotdeal.alarm.domain.usecase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
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.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.service.NotificationService
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 핫딜 스크래핑, DB 저장, 키워드 매칭, 알림 발송을 총괄하는 핵심 매니저
|
||||||
|
*
|
||||||
|
* 포그라운드(앱 진입/새로고침), 백그라운드(AlarmReceiver), WorkManager 모두에서
|
||||||
|
* 공통으로 직접 호출하여 지연 없이 즉시 수집 및 알림을 발송합니다.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class HotDealScraperManager @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val scraperFactory: ScraperFactory,
|
||||||
|
private val notificationService: NotificationService,
|
||||||
|
private val dealDao: HotDealDao,
|
||||||
|
private val siteConfigDao: SiteConfigDao,
|
||||||
|
private val keywordDao: KeywordDao
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "HotDealScraperManager"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 중복 동시 실행 방지 뮤텍스
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 핫딜 수집 및 키워드 알림 발송 전체 프로세스 실행
|
||||||
|
* @return 수집된 새로운 핫딜 목록
|
||||||
|
*/
|
||||||
|
suspend fun fetchAndProcessDeals(): List<HotDeal> = withContext(Dispatchers.IO) {
|
||||||
|
mutex.withLock {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "===== 🚀 핫딜 수집 프로세스 시작 =====")
|
||||||
|
|
||||||
|
// 1. 활성화된 사이트 설정 조회
|
||||||
|
val enabledConfigs = siteConfigDao.getEnabledConfigs()
|
||||||
|
Log.d(TAG, "활성화된 사이트 설정: ${enabledConfigs.size}개")
|
||||||
|
|
||||||
|
if (enabledConfigs.isEmpty()) {
|
||||||
|
Log.w(TAG, "활성화된 사이트 설정이 없습니다.")
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 활성화된 키워드 조회
|
||||||
|
val keywords = keywordDao.getEnabledKeywords()
|
||||||
|
Log.d(TAG, "활성화된 키워드: ${keywords.size}개 (${keywords.map { it.keyword }})")
|
||||||
|
|
||||||
|
// 3. 각 사이트/게시판 병렬 스크래핑
|
||||||
|
val allDeals = enabledConfigs.map { config ->
|
||||||
|
async { scrapeSite(config) }
|
||||||
|
}.awaitAll().flatten()
|
||||||
|
|
||||||
|
Log.d(TAG, "스크래핑 완료: 총 ${allDeals.size}개 핫딜 수집됨")
|
||||||
|
if (allDeals.isEmpty()) {
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 새로운 핫딜 필터링 (기존 DB에 없는 것)
|
||||||
|
val newDeals = filterNewDeals(allDeals)
|
||||||
|
Log.d(TAG, "새로운 핫딜: ${newDeals.size}개")
|
||||||
|
|
||||||
|
// 5. 인기글 상태 일괄 업데이트 (기존 + 신규 모두)
|
||||||
|
val popularDealIds = allDeals.filter { it.isPopular }.map { it.id }
|
||||||
|
if (popularDealIds.isNotEmpty()) {
|
||||||
|
dealDao.markAsPopular(popularDealIds)
|
||||||
|
Log.d(TAG, "🔥 인기글 마킹: ${popularDealIds.size}개")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newDeals.isNotEmpty()) {
|
||||||
|
// 6. 키워드 매칭
|
||||||
|
val keywordMatchedDeals = if (keywords.isNotEmpty()) {
|
||||||
|
filterByKeywords(newDeals, keywords).also {
|
||||||
|
Log.d(TAG, "⭐ 키워드 매칭 핫딜: ${it.size}개 -> ${it.map { d -> d.title }}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 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 개")
|
||||||
|
|
||||||
|
// 8. 키워드 매칭 알림 즉시 발송! (시스템바 노티피케이션)
|
||||||
|
if (keywordMatchedDeals.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "🔔 키워드 매칭 알림 발송: ${keywordMatchedDeals.size}개")
|
||||||
|
notificationService.showKeywordMatchNotification(keywordMatchedDeals)
|
||||||
|
dealDao.markAsKeywordMatch(keywordMatchedDeals.map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 알림 발송 상태 업데이트
|
||||||
|
dealDao.markAsNotified(newDeals.map { it.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 오래된 데이터 정리 (3일 이상)
|
||||||
|
val threshold = System.currentTimeMillis() - (3 * 24 * 60 * 60 * 1000L)
|
||||||
|
dealDao.deleteOldDeals(threshold)
|
||||||
|
|
||||||
|
Log.d(TAG, "===== ✅ 핫딜 수집 프로세스 종료 =====")
|
||||||
|
newDeals
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "핫딜 수집 오류: ${e.message}", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun scrapeSite(config: SiteConfigEntity): List<HotDeal> {
|
||||||
|
val scraper = scraperFactory.getScraper(config.siteName) ?: return emptyList()
|
||||||
|
val result = scraper.scrape(config.boardName)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { deals ->
|
||||||
|
try {
|
||||||
|
siteConfigDao.updateLastScrapedAt(config.siteBoardKey, System.currentTimeMillis())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 무시
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
Log.e(TAG, "${config.siteName}/${config.boardName} 실패: ${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