Initial commit: HotDeal Alarm Android App

Features:
- Multi-site hot deal scraping (Ppomppu, Clien, Ruriweb, Coolenjoy)
- Site filter with color-coded badges
- Board display names (e.g., ppomppu8 -> 알리뽐뿌)
- Anti-bot protection with request delays and User-Agent rotation
- Keyword matching and notifications
- Material Design 3 UI
This commit is contained in:
sanjeok77
2026-03-04 01:29:34 +09:00
commit 25a349f273
84 changed files with 6455 additions and 0 deletions
@@ -0,0 +1,199 @@
package com.hotdeal.alarm.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.ForegroundInfo
import com.hotdeal.alarm.R
import com.hotdeal.alarm.domain.model.HotDeal
import com.hotdeal.alarm.presentation.main.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* 알림 서비스
*/
@Singleton
class NotificationService @Inject constructor(
@ApplicationContext private val context: Context
) {
companion object {
const val CHANNEL_URGENT = "hotdeal_urgent"
const val CHANNEL_NORMAL = "hotdeal_normal"
const val CHANNEL_BACKGROUND = "hotdeal_background"
const val NOTIFICATION_ID_FOREGROUND = 1000
const val NOTIFICATION_ID_NEW_DEAL = 1001
const val NOTIFICATION_ID_KEYWORD = 1002
}
private val notificationManager = NotificationManagerCompat.from(context)
init {
createNotificationChannels()
}
/**
* 알림 채널 생성
*/
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channels = listOf(
NotificationChannel(
CHANNEL_URGENT,
context.getString(R.string.notification_channel_urgent),
NotificationManager.IMPORTANCE_HIGH
).apply {
description = context.getString(R.string.notification_channel_urgent_desc)
},
NotificationChannel(
CHANNEL_NORMAL,
context.getString(R.string.notification_channel_normal),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = context.getString(R.string.notification_channel_normal_desc)
},
NotificationChannel(
CHANNEL_BACKGROUND,
context.getString(R.string.notification_channel_background),
NotificationManager.IMPORTANCE_LOW
).apply {
description = context.getString(R.string.notification_channel_background_desc)
}
)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannels(channels)
}
}
/**
* 포그라운드 서비스용 Notification 생성
*/
fun createForegroundInfo(): ForegroundInfo {
val notification = NotificationCompat.Builder(context, CHANNEL_BACKGROUND)
.setContentTitle("핫딜 모니터링 중")
.setContentText("새로운 핫딜을 확인하고 있습니다")
.setSmallIcon(android.R.drawable.ic_menu_search)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
return ForegroundInfo(NOTIFICATION_ID_FOREGROUND, notification)
}
/**
* 새 핫딜 알림
*/
fun showNewDealsNotification(deals: List<HotDeal>) {
if (!hasNotificationPermission()) return
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = if (deals.size == 1) {
NotificationCompat.Builder(context, CHANNEL_NORMAL)
.setContentTitle(context.getString(R.string.notification_new_deal))
.setContentText(deals.first().title)
.setSmallIcon(android.R.drawable.ic_menu_send)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
} else {
NotificationCompat.Builder(context, CHANNEL_NORMAL)
.setContentTitle(context.getString(R.string.notification_new_deal))
.setContentText("새로운 핫딜 ${deals.size}")
.setSmallIcon(android.R.drawable.ic_menu_send)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(deals.take(5).joinToString("\n") { "${it.title}" })
)
.build()
}
notificationManager.notify(NOTIFICATION_ID_NEW_DEAL, notification)
}
/**
* 키워드 매칭 알림 (긴급)
*/
fun showKeywordMatchNotification(deals: List<HotDeal>) {
if (!hasNotificationPermission()) return
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_URGENT)
.setContentTitle(context.getString(R.string.notification_keyword_match))
.setContentText(deals.first().title)
.setSmallIcon(android.R.drawable.ic_menu_send)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(deals.take(5).joinToString("\n") { "${it.title}" })
)
.build()
notificationManager.notify(NOTIFICATION_ID_KEYWORD, notification)
}
/**
* 알림 권한 확인
*/
private fun hasNotificationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
} else {
true
}
}
/**
* 알림 채널 활성화 여부 확인
*/
fun isNotificationChannelEnabled(channelId: String): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = notificationManager.getNotificationChannel(channelId)
return channel?.importance != NotificationManager.IMPORTANCE_NONE
}
return true
}
/**
* 모든 알림 채널이 활성화되어 있는지 확인
*/
fun areAllChannelsEnabled(): Boolean {
return isNotificationChannelEnabled(CHANNEL_URGENT) &&
isNotificationChannelEnabled(CHANNEL_NORMAL)
}
/**
* 알림 권한이 있는지 확인 (public)
*/
fun checkNotificationPermission(): Boolean {
return hasNotificationPermission()
}
}