diff --git a/app/src/main/java/com/hotdeal/alarm/data/remote/scraper/PpomppuScraper.kt b/app/src/main/java/com/hotdeal/alarm/data/remote/scraper/PpomppuScraper.kt index f2e4a11..d9fd0f6 100644 --- a/app/src/main/java/com/hotdeal/alarm/data/remote/scraper/PpomppuScraper.kt +++ b/app/src/main/java/com/hotdeal/alarm/data/remote/scraper/PpomppuScraper.kt @@ -13,6 +13,8 @@ import kotlin.random.Random /** * 뽐뿌 스크래퍼 + * - hotlist_flag=999 페이지에서 인기글 ID를 수집하여 HOT 뱃지 표시 + * - 기존 hotpop_bg_color CSS 클래스도 폴백으로 감지 */ class PpomppuScraper(client: OkHttpClient) : BaseScraper(client) { @@ -25,18 +27,18 @@ class PpomppuScraper(client: OkHttpClient) : BaseScraper(client) { override suspend fun scrape(board: String): Result> = withContext(Dispatchers.IO) { try { + // 1단계: 인기글(hotlist) 페이지에서 인기 게시물 ID 목록 수집 + val hotPostIds = fetchHotPostIds(board) + Log.d("Ppomppu", "인기글 ID ${hotPostIds.size}개 수집됨: $hotPostIds") + + // 2단계: 일반 게시판 페이지 스크래핑 val url = getBoardUrl(board) Log.d("Ppomppu", "스크래핑 시작: $url") - // 요청 간격 랜덤화 (2~4초) - 차단 방지 val delayTime = Random.nextLong(2000, 4000) - Log.d("Ppomppu", "요청 대기: ${delayTime}ms") delay(delayTime) - // Jsoup으로 직접 연결 (User-Agent 회전) val userAgent = getRandomUserAgent() - Log.d("Ppomppu", "User-Agent: $userAgent") - val doc: Document = Jsoup.connect(url) .userAgent(userAgent) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -47,70 +49,91 @@ class PpomppuScraper(client: OkHttpClient) : BaseScraper(client) { .followRedirects(true) .get() - Log.d("Ppomppu", "문서 파싱 성공, 길이: ${doc.html().length}") - val deals = mutableListOf() + val rowElements: Elements = doc.select("tr.baseList") + Log.d("Ppomppu", "찾은 행 요소: ${rowElements.size}개") - // 셀렉터로 요소 찾기 - 인기 게시물 감지를 위해 tr 요소 선택 - val rowElements: Elements = doc.select("tr.baseList") - Log.d("Ppomppu", "찾은 행 요소: ${rowElements.size}개") + var count = 0 + rowElements.forEach { row -> + if (count >= 20) return@forEach - // 최대 20개까지만 처리 - var count = 0 - rowElements.forEach { row -> - if (count >= 20) return@forEach + try { + val titleElement = row.selectFirst("a.baseList-title") ?: return@forEach + val title = titleElement.text().trim() + if (title.isEmpty()) return@forEach - try { - // 인기 게시물 여부 확인 (hotpop_bg_color 클래스 존재 여부) - val isPopular = row.hasClass("hotpop_bg_color") + val href = titleElement.attr("href") + if (href.contains("regulation") || href.contains("notice")) return@forEach - // 제목 링크 찾기 - val titleElement = row.selectFirst("a.baseList-title") - if (titleElement == null) return@forEach + val postId = extractPostId(href) + if (postId.isEmpty()) { + Log.w("Ppomppu", "postId 추출 실패: href=$href") + return@forEach + } - val title = titleElement.text().trim() - if (title.isEmpty()) return@forEach + // 인기글 판단: hotlist 페이지에 포함되어 있거나 CSS 클래스로 감지 + val isPopular = postId in hotPostIds || row.hasClass("hotpop_bg_color") - val href = titleElement.attr("href") + val deal = HotDeal( + id = HotDeal.generateId(siteName, postId), + siteName = siteName, + boardName = board, + title = title, + url = resolveUrl(baseUrl, href), + createdAt = System.currentTimeMillis(), + isPopular = isPopular + ) + deals.add(deal) + count++ + val popularMark = if (isPopular) " [인기]" else "" + Log.d("Ppomppu", "[$count]$popularMark $title") + } catch (e: Exception) { + Log.e("Ppomppu", "파싱 에러: ${e.message}") + } + } - // 공지사항 제외 - if (href.contains("regulation") || href.contains("notice")) return@forEach - - val dealUrl = resolveUrl(baseUrl, href) - - // postId 추출 - val postId = extractPostId(href) - if (postId.isEmpty()) { - Log.w("Ppomppu", "postId 추출 실패: href=$href") - return@forEach - } - - val deal = HotDeal( - id = HotDeal.generateId(siteName, postId), - siteName = siteName, - boardName = board, - title = title, - url = dealUrl, - createdAt = System.currentTimeMillis(), - isPopular = isPopular - ) - deals.add(deal) - count++ - val popularMark = if (isPopular) " [인기]" else "" - Log.d("Ppomppu", "[$count]$popularMark $title") - } catch (e: Exception) { - Log.e("Ppomppu", "파싱 에러: ${e.message}") - } - } - - Log.d("Ppomppu", "파싱 완료: ${deals.size}개") + Log.d("Ppomppu", "파싱 완료: ${deals.size}개 (인기: ${deals.count { it.isPopular }}개)") Result.success(deals) } catch (e: Exception) { Log.e("Ppomppu", "스크래핑 실패: ${e.message}", e) Result.failure(e) } } - + + /** + * hotlist_flag=999 페이지에서 인기 게시물의 postId 목록을 수집 + */ + private suspend fun fetchHotPostIds(board: String): Set { + return try { + val hotUrl = "${baseUrl}zboard.php?id=$board&hotlist_flag=999" + Log.d("Ppomppu", "인기글 페이지 조회: $hotUrl") + + delay(Random.nextLong(1000, 2000)) + + val doc = Jsoup.connect(hotUrl) + .userAgent(getRandomUserAgent()) + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7") + .header("Referer", "https://www.ppomppu.co.kr/") + .timeout(15000) + .followRedirects(true) + .get() + + val ids = mutableSetOf() + doc.select("tr.baseList a.baseList-title").forEach { link -> + val href = link.attr("href") + val postId = extractPostId(href) + if (postId.isNotEmpty()) { + ids.add(postId) + } + } + ids + } catch (e: Exception) { + Log.w("Ppomppu", "인기글 목록 조회 실패 (폴백으로 계속): ${e.message}") + emptySet() + } + } + private fun extractPostId(href: String): String { val afterNo = href.substringAfter("no=", "") if (afterNo.isEmpty()) return ""