diff --git a/app/src/main/java/com/hotdeal/alarm/data/local/preferences/AppSettings.kt b/app/src/main/java/com/hotdeal/alarm/data/local/preferences/AppSettings.kt new file mode 100644 index 0000000..3987e16 --- /dev/null +++ b/app/src/main/java/com/hotdeal/alarm/data/local/preferences/AppSettings.kt @@ -0,0 +1,40 @@ +package com.hotdeal.alarm.data.local.preferences + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * 앱 설정 저장 (DataStore) + */ +class AppSettings(private val context: Context) { + + private val Context.dataStore: DataStore by preferencesDataStore(name = "app_settings") + + companion object { + private val POLLING_INTERVAL_KEY = intPreferencesKey("polling_interval_minutes") + private const val DEFAULT_INTERVAL = 2 + } + + /** + * 폴� 주기 (분) + */ + val pollingInterval: Flow = context.dataStore.data + .map { preferences -> + preferences[POLLING_INTERVAL_KEY] ?: DEFAULT_INTERVAL + } + + /** + * 폴� 주기 설정 저장 + */ + suspend fun setPollingInterval(minutes: Int) { + context.dataStore.edit { preferences -> + preferences[POLLING_INTERVAL_KEY] = minutes + } + } +} diff --git a/app/src/main/java/com/hotdeal/alarm/di/DatabaseModule.kt b/app/src/main/java/com/hotdeal/alarm/di/DatabaseModule.kt index d94cf7c..e0d01fc 100644 --- a/app/src/main/java/com/hotdeal/alarm/di/DatabaseModule.kt +++ b/app/src/main/java/com/hotdeal/alarm/di/DatabaseModule.kt @@ -6,6 +6,7 @@ import com.hotdeal.alarm.data.local.db.AppDatabase 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.preferences.AppSettings import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -52,4 +53,12 @@ object DatabaseModule { fun provideKeywordDao(database: AppDatabase): KeywordDao { return database.keywordDao() } + + @Provides + @Singleton + fun provideAppSettings( + @ApplicationContext context: Context + ): AppSettings { + return AppSettings(context) + } } diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt index 1e00d82..461a70d 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/components/DealItem.kt @@ -2,6 +2,7 @@ package com.hotdeal.alarm.presentation.components import androidx.compose.animation.core.* import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -9,14 +10,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.NotificationsActive import androidx.compose.material.icons.filled.Share -import androidx.compose.material.icons.filled.Star import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.hotdeal.alarm.domain.model.HotDeal @@ -24,7 +27,6 @@ import com.hotdeal.alarm.ui.theme.Spacing import com.hotdeal.alarm.ui.theme.getSiteColor import com.hotdeal.alarm.util.ShareHelper -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DealItem( deal: HotDeal, @@ -45,10 +47,34 @@ fun DealItem( val siteColor = getSiteColor(deal.siteType) + // 키워드 매칭된 핫딜은 다른 색상으로 강조 + val cardColor = if (deal.isKeywordMatch) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + } else { + MaterialTheme.colorScheme.surface + } + + val borderColor = if (deal.isKeywordMatch) { + MaterialTheme.colorScheme.primary.copy(alpha = 0.5f) + } else { + Color.Transparent + } + + val cardModifier = if (deal.isKeywordMatch) { + modifier + .fillMaxWidth() + .border(2.dp, borderColor, MaterialTheme.shapes.large) + } else { + modifier.fillMaxWidth() + } + ElevatedCard( - modifier = modifier.fillMaxWidth(), + modifier = cardModifier, shape = MaterialTheme.shapes.large, - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), + colors = CardDefaults.elevatedCardColors(containerColor = cardColor), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = if (deal.isKeywordMatch) 4.dp else 2.dp + ), onClick = onClick ) { Column( @@ -88,25 +114,43 @@ fun DealItem( Spacer(modifier = Modifier.width(Spacing.sm)) - // 게시판 이름 (ppomppu8 → 알리뽐뿌) + // 게시판 이름 Text( text = deal.boardDisplayName, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) - if (deal.isKeywordMatch) { - Spacer(modifier = Modifier.width(Spacing.xs)) - Icon( - imageVector = Icons.Default.Star, - contentDescription = "키워드 매칭", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(16.dp) - ) - } - Spacer(modifier = Modifier.weight(1f)) + // 키워드 매칭 배지 (있을 때만) + if (deal.isKeywordMatch) { + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.height(26.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Icon( + imageVector = Icons.Default.NotificationsActive, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(14.dp) + ) + Text( + text = "키워드 매칭", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } + Spacer(modifier = Modifier.width(Spacing.xs)) + } + // 공유 버튼 IconButton( onClick = { ShareHelper.shareDeal(context, deal) }, @@ -139,10 +183,14 @@ fun DealItem( Spacer(modifier = Modifier.height(Spacing.sm)) - // 제목 + // 제목 (키워드 매칭 시 굵게) Text( text = deal.title, - style = MaterialTheme.typography.bodyLarge, + style = if (deal.isKeywordMatch) { + MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) + } else { + MaterialTheme.typography.bodyLarge + }, maxLines = 2, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt index 459fd4e..d45c524 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/main/MainViewModel.kt @@ -6,6 +6,7 @@ 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.SiteConfigEntity +import com.hotdeal.alarm.data.local.preferences.AppSettings import com.hotdeal.alarm.domain.model.SiteType import com.hotdeal.alarm.worker.WorkerScheduler import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,12 +19,17 @@ class MainViewModel @Inject constructor( private val hotDealDao: HotDealDao, private val siteConfigDao: SiteConfigDao, private val keywordDao: KeywordDao, - private val workerScheduler: WorkerScheduler + private val workerScheduler: WorkerScheduler, + private val appSettings: AppSettings ) : ViewModel() { private val _uiState = MutableStateFlow(MainUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() + // 폴� 주기 + val pollingInterval: StateFlow = appSettings.pollingInterval + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2) + init { initializeApp() } @@ -32,6 +38,9 @@ class MainViewModel @Inject constructor( viewModelScope.launch { initializeDefaultSiteConfigs() loadState() + // 저장된 폴� 주기로 시작 + val savedInterval = appSettings.pollingInterval.first() + startPolling(savedInterval.toLong()) } } @@ -105,8 +114,14 @@ class MainViewModel @Inject constructor( workerScheduler.executeOnce() } + /** + * 폴� 시작 (주기 저장) + */ fun startPolling(intervalMinutes: Long = WorkerScheduler.DEFAULT_INTERVAL_MINUTES) { - workerScheduler.schedulePeriodicPolling(intervalMinutes) + viewModelScope.launch { + appSettings.setPollingInterval(intervalMinutes.toInt()) + workerScheduler.schedulePeriodicPolling(intervalMinutes) + } } fun stopPolling() { diff --git a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt index 41a93cf..09f776d 100644 --- a/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/hotdeal/alarm/presentation/settings/SettingsScreen.kt @@ -26,7 +26,6 @@ import com.hotdeal.alarm.presentation.main.MainUiState import com.hotdeal.alarm.presentation.main.MainViewModel import com.hotdeal.alarm.util.PermissionHelper import com.hotdeal.alarm.util.VersionManager -import com.hotdeal.alarm.worker.WorkerScheduler import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -35,6 +34,7 @@ fun SettingsScreen(viewModel: MainViewModel) { val context = LocalContext.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + val currentPollingInterval by viewModel.pollingInterval.collectAsState() var showPermissionDialog by remember { mutableStateOf(false) } var permissionDialogTitle by remember { mutableStateOf("") } @@ -47,28 +47,23 @@ fun SettingsScreen(viewModel: MainViewModel) { if (!isGranted) { permissionDialogTitle = "알림 권한 필요" permissionDialogMessage = "핫딜 알림을 받으려면 알림 권한이 필요합니다." - permissionDialogAction = { - PermissionHelper.openNotificationSettings(context) - } + permissionDialogAction = { PermissionHelper.openNotificationSettings(context) } showPermissionDialog = true } } val permissionStatus = PermissionHelper.checkAllPermissions(context) - val hasEnabledSites = (uiState as? MainUiState.Success) - ?.siteConfigs?.any { it.isEnabled } ?: false + val hasEnabledSites = (uiState as? MainUiState.Success)?.siteConfigs?.any { it.isEnabled } ?: false LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { item { - PermissionStatusSection( + PermissionCard( permissionStatus = permissionStatus, hasEnabledSites = hasEnabledSites, - onRequestNotificationPermission = { + onRequestPermission = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } @@ -77,29 +72,26 @@ fun SettingsScreen(viewModel: MainViewModel) { } item { - PollingIntervalSection( - currentInterval = 2, + PollingIntervalCard( + currentInterval = currentPollingInterval, onIntervalChange = { minutes -> viewModel.stopPolling() viewModel.startPolling(minutes) + Toast.makeText(context, "${minutes}분으로 변경됨", Toast.LENGTH_SHORT).show() } ) } - item { - Text("사이트 선택", style = MaterialTheme.typography.titleMedium) - } + item { Text("사이트 선택", style = MaterialTheme.typography.titleMedium) } when (val state = uiState) { is MainUiState.Success -> { SiteType.entries.forEach { site -> item { - SiteSection( + SiteCard( siteType = site, configs = state.siteConfigs.filter { it.siteName == site.name }, - onToggle = { key, enabled -> - viewModel.toggleSiteConfig(key, enabled) - } + onToggle = { key, enabled -> viewModel.toggleSiteConfig(key, enabled) } ) } } @@ -111,10 +103,7 @@ fun SettingsScreen(viewModel: MainViewModel) { item { var keywordText by remember { mutableStateOf("") } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { + Row(verticalAlignment = Alignment.CenterVertically) { OutlinedTextField( value = keywordText, onValueChange = { keywordText = it }, @@ -144,16 +133,14 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } - // 버전 정보 섹션 item { Spacer(modifier = Modifier.height(24.dp)) - VersionSection( + VersionCard( currentVersion = VersionManager.getCurrentVersion(context), onCheckUpdate = { scope.launch { val currentCode = VersionManager.getCurrentVersionCode(context) val remoteInfo = VersionManager.checkForUpdate() - if (remoteInfo != null && VersionManager.isUpdateAvailable(currentCode, remoteInfo.versionCode)) { Toast.makeText(context, "새 버전 ${remoteInfo.version} 사용 가능", Toast.LENGTH_LONG).show() } else { @@ -164,9 +151,7 @@ fun SettingsScreen(viewModel: MainViewModel) { ) } } - else -> { - item { CircularProgressIndicator() } - } + else -> { item { CircularProgressIndicator() } } } } @@ -181,33 +166,47 @@ fun SettingsScreen(viewModel: MainViewModel) { } @Composable -fun VersionSection(currentVersion: String, onCheckUpdate: () -> Unit) { +fun PermissionCard( + permissionStatus: PermissionHelper.PermissionStatus, + hasEnabledSites: Boolean, + onRequestPermission: () -> Unit +) { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text("앱 버전", style = MaterialTheme.typography.labelMedium) - Text("v$currentVersion", style = MaterialTheme.typography.titleMedium) - } - Button(onClick = onCheckUpdate) { - Text("업데이트 확인") + Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.width(12.dp)) + Text("알림 권한", modifier = Modifier.weight(1f)) + if (!permissionStatus.hasNotificationPermission) { + Button(onClick = onRequestPermission) { Text("설정") } } } + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.width(12.dp)) + Text("사이트 선택") + } } } } @Composable -fun PollingIntervalSection(currentInterval: Int, onIntervalChange: (Long) -> Unit) { - var selected by remember { mutableStateOf(currentInterval) } +fun PollingIntervalCard(currentInterval: Int, onIntervalChange: (Long) -> Unit) { + var selected by remember(currentInterval) { mutableStateOf(currentInterval) } Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Text("폴� 주기", style = MaterialTheme.typography.titleMedium) - Text("2분 권장 (배터리/데이터 절약)", style = MaterialTheme.typography.bodySmall) + Text("2분 권장", style = MaterialTheme.typography.bodySmall) Spacer(modifier = Modifier.height(8.dp)) listOf(1, 2, 5, 10, 15, 30).forEach { minutes -> Row(verticalAlignment = Alignment.CenterVertically) { @@ -234,44 +233,7 @@ fun PollingIntervalSection(currentInterval: Int, onIntervalChange: (Long) -> Uni } @Composable -fun PermissionStatusSection( - permissionStatus: PermissionHelper.PermissionStatus, - hasEnabledSites: Boolean, - onRequestNotificationPermission: () -> Unit -) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("알림 설정", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = if (permissionStatus.hasNotificationPermission) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - tint = if (permissionStatus.hasNotificationPermission) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("알림 권한", modifier = Modifier.weight(1f)) - if (!permissionStatus.hasNotificationPermission) { - Button(onClick = onRequestNotificationPermission) { Text("설정") } - } - } - - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = if (hasEnabledSites) Icons.Default.Check else Icons.Default.Close, - contentDescription = null, - tint = if (hasEnabledSites) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.width(12.dp)) - Text("사이트 선택", modifier = Modifier.weight(1f)) - } - } - } -} - -@Composable -fun SiteSection(siteType: SiteType, configs: List, onToggle: (String, Boolean) -> Unit) { +fun SiteCard(siteType: SiteType, configs: List, onToggle: (String, Boolean) -> Unit) { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Text(siteType.displayName, style = MaterialTheme.typography.titleSmall) @@ -305,3 +267,22 @@ fun KeywordItem(keyword: Keyword, onToggle: () -> Unit, onDelete: () -> Unit) { } ) } + +@Composable +fun VersionCard(currentVersion: String, onCheckUpdate: () -> Unit) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text("앱 버전", style = MaterialTheme.typography.labelMedium) + Text("v$currentVersion", style = MaterialTheme.typography.titleMedium) + } + Button(onClick = onCheckUpdate) { Text("업데이트 확인") } + } + } + } +} 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 40fb76c..170571e 100644 --- a/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt +++ b/app/src/main/java/com/hotdeal/alarm/worker/HotDealPollingWorker.kt @@ -21,6 +21,8 @@ import kotlinx.coroutines.withContext /** * 핫딜 폴� Worker + * + * 키워드 매칭된 핫딜만 알림 발송 */ @HiltWorker class HotDealPollingWorker @AssistedInject constructor( @@ -47,20 +49,13 @@ class HotDealPollingWorker @AssistedInject constructor( Log.d(TAG, "활성화된 사이트 설정: ${enabledConfigs.size}개") if (enabledConfigs.isEmpty()) { - Log.w(TAG, "활성화된 사이트 설정이 없습니다. 폴�을 건�뜁니다.") + Log.w(TAG, "활성화된 사이트 설정이 없습니다.") return@withContext Result.success() } - - enabledConfigs.forEach { config -> - Log.d(TAG, " - 사이트: ${config.siteName}, 게시판: ${config.boardName}") - } // 2. 활성화된 키워드 조회 val keywords = keywordDao.getEnabledKeywords() Log.d(TAG, "활성화된 키워드: ${keywords.size}개") - keywords.forEach { kw -> - Log.d(TAG, " - 키워드: ${kw.keyword}") - } // 3. 각 사이트/게시판 스크래핑 (병렬) Log.d(TAG, "스크래핑 시작...") @@ -74,11 +69,10 @@ class HotDealPollingWorker @AssistedInject constructor( if (allDeals.isEmpty()) { Log.w(TAG, "스크래핑된 핫딜이 없습니다.") - // 스크래핑은 성공했지만 결과가 없는 경우도 success로 처리 return@withContext Result.success() } - // 4. 새로운 핫딜 필터링 (URL과 ID 모두로 중복 체크) + // 4. 새로운 핫딜 필터링 val newDeals = filterNewDeals(allDeals) Log.d(TAG, "새로운 핫딜: ${newDeals.size}개") @@ -87,15 +81,17 @@ class HotDealPollingWorker @AssistedInject constructor( return@withContext Result.success() } - newDeals.take(5).forEach { deal -> - Log.d(TAG, " 새 핫딜: ${deal.title.take(50)}...") + // 5. 키워드 매칭 (키워드가 있을 때만) + val keywordMatchedDeals = if (keywords.isNotEmpty()) { + filterByKeywords(newDeals, keywords).also { + Log.d(TAG, "키워드 매칭 핫딜: ${it.size}개") + } + } else { + Log.d(TAG, "키워드가 없어 매칭 체크를 건�뜁니다.") + emptyList() } - // 5. 키워드 매칭 - val keywordMatchedDeals = filterByKeywords(newDeals, keywords) - Log.d(TAG, "키워드 매칭 핫딜: ${keywordMatchedDeals.size}개") - - // 6. DB 저장 (개별 삽입으로 변경하여 충돌 처리 개선) + // 6. DB 저장 var insertedCount = 0 newDeals.forEach { deal -> try { @@ -103,19 +99,18 @@ class HotDealPollingWorker @AssistedInject constructor( dealDao.insertDeal(entity) insertedCount++ } catch (e: Exception) { - Log.w(TAG, "핫딜 저장 실패 (무시됨): ${deal.id}, 오류: ${e.message}") + Log.w(TAG, "핫딜 저장 실패 (무시됨): ${deal.id}") } } Log.d(TAG, "DB 저장 완료: $insertedCount/${newDeals.size}개 저장됨") - // 7. 알림 발송 + // 7. 키워드 매칭 알림 발송 (키워드 매칭된 것만!) if (keywordMatchedDeals.isNotEmpty()) { - Log.d(TAG, "키워드 매칭 알림 발송...") + Log.d(TAG, "키워드 매칭 알림 발송: ${keywordMatchedDeals.size}개") notificationService.showKeywordMatchNotification(keywordMatchedDeals) dealDao.markAsKeywordMatch(keywordMatchedDeals.map { it.id }) } else { - Log.d(TAG, "새 핫딜 알림 발송...") - notificationService.showNewDealsNotification(newDeals.take(5)) + Log.d(TAG, "키워드 매칭된 핫딜이 없어 알림을 발송하지 않습니다.") } // 8. 알림 발송 상태 업데이트 @@ -124,7 +119,6 @@ class HotDealPollingWorker @AssistedInject constructor( // 9. 오래된 데이터 정리 (7일 이상) val threshold = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L) dealDao.deleteOldDeals(threshold) - Log.d(TAG, "오래된 데이터 정리 완료") Log.d(TAG, "===== 핫딜 폴� 완료 =====") Result.success() @@ -138,16 +132,10 @@ class HotDealPollingWorker @AssistedInject constructor( } } - /** - * 포그라운드 서비스 정보 (빈번한 폴�용) - */ override suspend fun getForegroundInfo(): ForegroundInfo { return notificationService.createForegroundInfo() } - /** - * 사이트 스크래핑 - */ private suspend fun scrapeSite(config: SiteConfigEntity): List { val scraper = scraperFactory.getScraper(config.siteName) if (scraper == null) { @@ -161,7 +149,6 @@ class HotDealPollingWorker @AssistedInject constructor( result.fold( onSuccess = { deals -> Log.d(TAG, " ${config.siteName}: ${deals.size}개 핫딜 파싱됨") - // 스크래핑 성공 시 lastScrapedAt 업데이트 try { siteConfigDao.updateLastScrapedAt(config.siteBoardKey, System.currentTimeMillis()) } catch (e: Exception) { @@ -176,17 +163,11 @@ class HotDealPollingWorker @AssistedInject constructor( return result.getOrElse { emptyList() } } - /** - * 새로운 핫딜 필터링 - * URL과 ID 모두로 중복 체크 - */ private suspend fun filterNewDeals(deals: List): List { return deals.filter { deal -> - // URL로 먼저 체크 val existingByUrl = dealDao.getDealByUrl(deal.url) if (existingByUrl != null) return@filter false - // ID로도 체크 (추가 안전장치) val existingById = dealDao.getDealById(deal.id) if (existingById != null) return@filter false @@ -194,15 +175,10 @@ class HotDealPollingWorker @AssistedInject constructor( } } - /** - * 키워드 매칭 필터링 - */ - private suspend fun filterByKeywords(deals: List, keywords: List): List { - if (keywords.isEmpty()) return emptyList() - + private fun filterByKeywords(deals: List, keywords: List): List { return deals.filter { deal -> keywords.any { keyword -> - keyword.toDomain().matches(deal.title) + deal.title.contains(keyword.keyword, ignoreCase = true) } } }