Instantly share code, notes, and snippets.
Last active
April 21, 2026 03:03
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save listly-io/c4ae6706b3bb6d436c882d46d2b8776b to your computer and use it in GitHub Desktop.
naver_smartstore_review_2026-04-21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==================== 리뷰 수집기 (모달 + 무한스크롤 모드) ==================== | |
| // | |
| // [사용법] | |
| // 1. 상품 상세 페이지에서 이 스크립트를 콘솔에 붙여넣기 | |
| // → "리뷰 전체보기" 버튼을 자동 클릭 → 모달 오픈 → 무한 스크롤로 수집 | |
| // 2. 수집 완료 후 화면 우측 하단 패널에서 원하는 영역의 "데이터 불러오기" 클릭 | |
| // 3. 패널 위에 나타난 파란 박스를 Listly Parts로 선택하여 추출 | |
| // 4. 추출 완료 후 "내리기" 클릭 → 다음 영역 반복 | |
| // | |
| // ==================== 설정 변수 ==================== | |
| // 최대 스크롤 시도 횟수 | |
| const MAX_SCROLL_COUNT = 500; | |
| // 모달 오픈 후 대기 시간 (밀리초) | |
| const MODAL_OPEN_WAIT_TIME = 1500; | |
| // 스크롤 후 대기 시간 (밀리초) | |
| const SCROLL_WAIT_TIME = 1000; | |
| // 새 리뷰가 안 나오는 스크롤이 연속 N회면 수집 종료 | |
| const MAX_EMPTY_SCROLLS = 4; | |
| // 수집 영역 1개당 최대 리뷰 수 (이 수를 초과하면 새 영역 생성) | |
| const MAX_REVIEWS_PER_COLLECTOR = 2000; | |
| // CSS Selectors | |
| const REVIEW_ITEM_SELECTOR = '[id^="REVIEW_ITEM_"]'; | |
| const MODAL_ROOT_SELECTOR = '#MODAL_ROOT_ID'; | |
| const OPEN_MODAL_BUTTON_SELECTOR = '#REVIEW button[data-shp-area="sprvrpre.more"][data-shp-area-id="more"]'; | |
| // ==================== 전역 변수 ==================== | |
| let scrollCycleCount = 0; | |
| let collectorIndex = 1; | |
| let currentCollectorCount = 0; | |
| const collectors = []; | |
| let activeCollector = null; | |
| // 무한스크롤에서는 이전 리뷰가 DOM에 계속 남아있으므로 ID 기반 dedup 필수 | |
| const collectedReviewIds = new Set(); | |
| let isCollecting = false; | |
| let statusDiv = null; | |
| // ==================== 유틸리티 함수 ==================== | |
| function wait(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| // ==================== 메모리 기반 수집 영역 관리 ==================== | |
| function createNewCollector(index) { | |
| const collector = { | |
| index: index, | |
| htmlChunks: [], | |
| count: 0 | |
| }; | |
| collectors.push(collector); | |
| console.log(`✅ 수집 영역 #${index} 생성 (메모리 저장, 최대 ${MAX_REVIEWS_PER_COLLECTOR}개)`); | |
| return collector; | |
| } | |
| // 상태 표시 UI 초기화 | |
| function initStatusUI() { | |
| statusDiv = document.getElementById('review-collector-status'); | |
| if (!statusDiv) { | |
| statusDiv = document.createElement('div'); | |
| statusDiv.id = 'review-collector-status'; | |
| statusDiv.style.cssText = ` | |
| position: fixed; | |
| bottom: 80px; | |
| right: 30px; | |
| background: #1e1e1e; | |
| color: #fff; | |
| padding: 0; | |
| border-radius: 12px; | |
| font-size: 13px; | |
| z-index: 2147483647; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.5); | |
| width: 320px; | |
| max-height: 80vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| `; | |
| document.body.appendChild(statusDiv); | |
| } | |
| updateStatusUI(); | |
| } | |
| function injectStyles() { | |
| if (document.getElementById('review-collector-styles')) return; | |
| const style = document.createElement('style'); | |
| style.id = 'review-collector-styles'; | |
| style.textContent = ` | |
| #review-collector-status * { box-sizing: border-box; } | |
| .rc-header { | |
| padding: 14px 16px; | |
| background: #2a2a2a; | |
| border-radius: 12px 12px 0 0; | |
| border-bottom: 1px solid #3a3a3a; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| cursor: move; | |
| } | |
| .rc-header-title { font-size: 14px; font-weight: 700; } | |
| .rc-header-count { font-size: 13px; color: #4fc3f7; } | |
| .rc-body { | |
| padding: 8px; | |
| overflow-y: auto; | |
| max-height: 50vh; | |
| flex: 1; | |
| } | |
| .rc-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 10px; | |
| border-radius: 8px; | |
| margin-bottom: 4px; | |
| background: #2a2a2a; | |
| transition: background 0.15s; | |
| } | |
| .rc-row:hover { background: #333; } | |
| .rc-row-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .rc-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 5px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| flex-shrink: 0; | |
| } | |
| .rc-badge-mem { background: #37474f; color: #90a4ae; } | |
| .rc-badge-dom { background: #1b5e20; color: #66bb6a; } | |
| .rc-count { font-size: 12px; color: #aaa; } | |
| .rc-actions { display: flex; gap: 4px; flex-shrink: 0; } | |
| .rc-btn { | |
| padding: 4px 10px; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.15s; | |
| white-space: nowrap; | |
| } | |
| .rc-btn:active { transform: scale(0.95); } | |
| .rc-btn-mount { background: #1565c0; color: #fff; } | |
| .rc-btn-mount:hover { background: #1976d2; } | |
| .rc-btn-unmount { background: #e65100; color: #fff; } | |
| .rc-btn-unmount:hover { background: #ef6c00; } | |
| .rc-status-tag { | |
| font-size: 10px; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| } | |
| .rc-tag-mem { background: #37474f; color: #90a4ae; } | |
| .rc-tag-dom { background: #1b5e20; color: #66bb6a; } | |
| .rc-collecting-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 12px; | |
| background: #1a237e; | |
| font-size: 12px; | |
| color: #8c9eff; | |
| } | |
| .rc-spinner { | |
| width: 14px; | |
| height: 14px; | |
| border: 2px solid #555; | |
| border-top-color: #4fc3f7; | |
| border-radius: 50%; | |
| animation: rc-spin 0.8s linear infinite; | |
| } | |
| @keyframes rc-spin { to { transform: rotate(360deg); } } | |
| .rc-minimize-btn { | |
| background: none; | |
| border: none; | |
| color: #888; | |
| cursor: pointer; | |
| font-size: 16px; | |
| padding: 0 4px; | |
| line-height: 1; | |
| } | |
| .rc-minimize-btn:hover { color: #fff; } | |
| /* 페이지 내에 표시되는 마운트된 수집 영역 */ | |
| .review-collector-area { | |
| position: fixed; | |
| right: 30px; | |
| width: 320px; | |
| height: 100px; | |
| border: 3px dashed #1565c0; | |
| border-radius: 12px; | |
| background: #e3f2fd; | |
| z-index: 2147483647; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| cursor: pointer; | |
| transition: border-color 0.2s, background 0.2s; | |
| } | |
| .review-collector-area:hover { | |
| border-color: #0d47a1; | |
| background: #bbdefb; | |
| } | |
| .review-collector-area .rc-mount-label { | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: #1565c0; | |
| } | |
| .review-collector-area .rc-mount-guide { | |
| font-size: 12px; | |
| color: #1976d2; | |
| background: rgba(21,101,192,0.1); | |
| padding: 4px 12px; | |
| border-radius: 6px; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function updateStatusUI() { | |
| if (!statusDiv) return; | |
| injectStyles(); | |
| const totalCount = collectors.reduce((sum, c) => sum + c.count, 0); | |
| let html = ''; | |
| // 헤더 | |
| html += `<div class="rc-header"> | |
| <div> | |
| <div class="rc-header-title">📦 리뷰 수집기</div> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:10px;"> | |
| <span class="rc-header-count">${totalCount.toLocaleString()}개</span> | |
| </div> | |
| </div>`; | |
| // 수집 중 표시 | |
| if (isCollecting) { | |
| html += `<div class="rc-collecting-indicator"> | |
| <div class="rc-spinner"></div> | |
| 수집 중... (스크롤 ${scrollCycleCount}회) | |
| </div>`; | |
| } | |
| // 영역 목록 | |
| html += `<div class="rc-body">`; | |
| if (collectors.length === 0) { | |
| html += `<div style="text-align:center;padding:20px;color:#666;">수집된 리뷰가 없습니다</div>`; | |
| } else { | |
| collectors.forEach(c => { | |
| const isMounted = !!document.getElementById(`review-collector-${c.index}`); | |
| let statusTag, badgeClass; | |
| if (isMounted) { | |
| statusTag = `<span class="rc-status-tag rc-tag-dom">불러옴</span>`; | |
| badgeClass = 'rc-badge-dom'; | |
| } else { | |
| statusTag = `<span class="rc-status-tag rc-tag-mem">대기</span>`; | |
| badgeClass = 'rc-badge-mem'; | |
| } | |
| html += `<div class="rc-row"> | |
| <div class="rc-row-info"> | |
| <span class="rc-badge ${badgeClass}">${c.index}</span> | |
| <span class="rc-count">${c.count.toLocaleString()}개</span> | |
| ${statusTag} | |
| </div> | |
| <div class="rc-actions">`; | |
| if (isMounted) { | |
| html += `<button class="rc-btn rc-btn-unmount" onclick="unmountCollector(${c.index}); updateStatusUI();">내리기</button>`; | |
| } else { | |
| html += `<button class="rc-btn rc-btn-mount" onclick="mountCollector(${c.index}); updateStatusUI();">데이터 불러오기</button>`; | |
| } | |
| html += `</div></div>`; | |
| }); | |
| } | |
| html += `</div>`; | |
| statusDiv.innerHTML = html; | |
| } | |
| // 수집 영역 초기화 (기존 DOM/레거시 마이그레이션 포함) | |
| function initCollectors() { | |
| // 기존 DOM 기반 수집 영역이 있으면 메모리로 마이그레이션 | |
| const existingAreas = document.querySelectorAll('.review-collector-area'); | |
| if (existingAreas.length > 0) { | |
| console.log(`🔄 기존 DOM 수집 영역 ${existingAreas.length}개를 메모리로 마이그레이션합니다...`); | |
| existingAreas.forEach((area, idx) => { | |
| const areaIndex = idx + 1; | |
| const reviews = area.querySelectorAll(REVIEW_ITEM_SELECTOR + ', li[data-shp-area-id="review"]'); | |
| const collector = { | |
| index: areaIndex, | |
| htmlChunks: [], | |
| count: reviews.length | |
| }; | |
| reviews.forEach(review => { | |
| collector.htmlChunks.push(review.outerHTML); | |
| // 마이그레이션 시에도 ID 추출하여 dedup 세트에 추가 | |
| const idMatch = review.id && review.id.match(/REVIEW_ITEM_\d+/); | |
| if (idMatch) collectedReviewIds.add(idMatch[0]); | |
| }); | |
| collectors.push(collector); | |
| console.log(` 영역 #${areaIndex}: ${reviews.length}개 마이그레이션 완료`); | |
| area.remove(); | |
| }); | |
| collectorIndex = collectors.length; | |
| activeCollector = collectors[collectors.length - 1]; | |
| currentCollectorCount = activeCollector.count; | |
| if (currentCollectorCount >= MAX_REVIEWS_PER_COLLECTOR) { | |
| collectorIndex++; | |
| currentCollectorCount = 0; | |
| activeCollector = createNewCollector(collectorIndex); | |
| } | |
| return; | |
| } | |
| // 레거시 호환: 이전 버전의 단일 #review-collector | |
| const legacyDiv = document.getElementById('review-collector'); | |
| if (legacyDiv) { | |
| const reviews = legacyDiv.querySelectorAll(REVIEW_ITEM_SELECTOR + ', li[data-shp-area-id="review"]'); | |
| const collector = { | |
| index: 1, | |
| htmlChunks: [], | |
| count: reviews.length | |
| }; | |
| reviews.forEach(review => { | |
| collector.htmlChunks.push(review.outerHTML); | |
| const idMatch = review.id && review.id.match(/REVIEW_ITEM_\d+/); | |
| if (idMatch) collectedReviewIds.add(idMatch[0]); | |
| }); | |
| collectors.push(collector); | |
| legacyDiv.remove(); | |
| console.log(`✅ 레거시 수집 영역 마이그레이션 → 영역 #1 (${reviews.length}개)`); | |
| collectorIndex = 1; | |
| activeCollector = collector; | |
| currentCollectorCount = collector.count; | |
| if (currentCollectorCount >= MAX_REVIEWS_PER_COLLECTOR) { | |
| collectorIndex++; | |
| currentCollectorCount = 0; | |
| activeCollector = createNewCollector(collectorIndex); | |
| } | |
| return; | |
| } | |
| // 아무것도 없으면 새로 생성 | |
| currentCollectorCount = 0; | |
| activeCollector = createNewCollector(collectorIndex); | |
| } | |
| function ensureCollectorCapacity() { | |
| if (currentCollectorCount >= MAX_REVIEWS_PER_COLLECTOR) { | |
| console.log(`📦 수집 영역 #${collectorIndex}가 ${currentCollectorCount}개로 가득 찼습니다.`); | |
| collectorIndex++; | |
| currentCollectorCount = 0; | |
| activeCollector = createNewCollector(collectorIndex); | |
| } | |
| } | |
| // ==================== 모달 제어 ==================== | |
| function getModalRoot() { | |
| return document.querySelector(MODAL_ROOT_SELECTOR); | |
| } | |
| function isModalOpen() { | |
| const root = getModalRoot(); | |
| if (!root) return false; | |
| // 모달이 실제로 열려있는지 확인 (내용이 있는지) | |
| return root.querySelector(REVIEW_ITEM_SELECTOR) !== null || | |
| root.children.length > 0; | |
| } | |
| async function openReviewModal() { | |
| if (isModalOpen()) { | |
| console.log('ℹ️ 리뷰 모달이 이미 열려있습니다.'); | |
| return true; | |
| } | |
| // "리뷰 전체보기" 버튼 찾기 | |
| let openBtn = document.querySelector(OPEN_MODAL_BUTTON_SELECTOR); | |
| // 첫 번째 셀렉터가 실패하면 텍스트로 폴백 | |
| if (!openBtn) { | |
| const candidates = document.querySelectorAll('#REVIEW button'); | |
| for (const btn of candidates) { | |
| if ((btn.textContent || '').includes('리뷰 전체보기')) { | |
| openBtn = btn; | |
| break; | |
| } | |
| } | |
| } | |
| if (!openBtn) { | |
| console.log('❌ "리뷰 전체보기" 버튼을 찾을 수 없습니다.'); | |
| return false; | |
| } | |
| // 버튼이 보이는 위치로 스크롤한 뒤 클릭 | |
| openBtn.scrollIntoView({ behavior: 'auto', block: 'center' }); | |
| await wait(300); | |
| openBtn.click(); | |
| console.log('🖱️ "리뷰 전체보기" 버튼 클릭 → 모달 로딩 대기'); | |
| // 모달이 실제로 열릴 때까지 대기 (최대 10초) | |
| const maxWait = 10000; | |
| const pollInterval = 200; | |
| const startTime = Date.now(); | |
| while (Date.now() - startTime < maxWait) { | |
| if (isModalOpen()) { | |
| console.log('✅ 리뷰 모달 오픈 확인'); | |
| await wait(MODAL_OPEN_WAIT_TIME); | |
| return true; | |
| } | |
| await wait(pollInterval); | |
| } | |
| console.log('⚠️ 모달 오픈 대기 시간 초과'); | |
| return false; | |
| } | |
| // 모달 내부에서 스크롤 가능한 컨테이너 찾기 | |
| function findScrollableContainer() { | |
| const modalRoot = getModalRoot(); | |
| if (!modalRoot) return null; | |
| let bestCandidate = null; | |
| let maxScrollable = 0; | |
| const walk = (el) => { | |
| const style = window.getComputedStyle(el); | |
| const overflowY = style.overflowY; | |
| if ((overflowY === 'auto' || overflowY === 'scroll')) { | |
| const scrollable = el.scrollHeight - el.clientHeight; | |
| if (scrollable > 10 && scrollable > maxScrollable) { | |
| maxScrollable = scrollable; | |
| bestCandidate = el; | |
| } | |
| } | |
| for (const child of el.children) walk(child); | |
| }; | |
| walk(modalRoot); | |
| // 폴백: 모달 자체가 스크롤 가능할 수도 있음 | |
| if (!bestCandidate) { | |
| if (modalRoot.scrollHeight > modalRoot.clientHeight + 10) { | |
| return modalRoot; | |
| } | |
| // 최후 폴백: window | |
| return null; | |
| } | |
| return bestCandidate; | |
| } | |
| // 스크롤을 바닥으로 내리고 새 콘텐츠 로드 대기 | |
| async function scrollToBottomAndWait() { | |
| const container = findScrollableContainer(); | |
| // 모달 내 전용 스크롤 컨테이너가 없으면 window 스크롤 | |
| if (!container) { | |
| const prevScrollY = window.scrollY; | |
| const prevDocHeight = document.documentElement.scrollHeight; | |
| window.scrollTo(0, document.documentElement.scrollHeight); | |
| await wait(SCROLL_WAIT_TIME); | |
| return { | |
| scrolled: window.scrollY > prevScrollY, | |
| heightGrew: document.documentElement.scrollHeight > prevDocHeight | |
| }; | |
| } | |
| const prevScrollTop = container.scrollTop; | |
| const prevScrollHeight = container.scrollHeight; | |
| container.scrollTop = container.scrollHeight; | |
| await wait(SCROLL_WAIT_TIME); | |
| return { | |
| scrolled: container.scrollTop > prevScrollTop, | |
| heightGrew: container.scrollHeight > prevScrollHeight | |
| }; | |
| } | |
| // 모달 닫기 버튼 셀렉터 | |
| const CLOSE_MODAL_BUTTON_SELECTOR = 'button[data-shp-area="sprvarvs_l.close"][data-shp-area-id="close"]'; | |
| // 모달 닫기 | |
| function closeReviewModal() { | |
| const btn = document.querySelector(CLOSE_MODAL_BUTTON_SELECTOR); | |
| if (!btn) { | |
| console.log('ℹ️ 모달 닫기 버튼을 찾을 수 없습니다.'); | |
| return false; | |
| } | |
| btn.click(); | |
| console.log('🚪 모달 닫기 버튼 클릭'); | |
| return true; | |
| } | |
| // ==================== 리뷰 수집 ==================== | |
| function collectReviews() { | |
| const modalRoot = getModalRoot(); | |
| const searchRoot = modalRoot || document; | |
| const reviews = searchRoot.querySelectorAll(REVIEW_ITEM_SELECTOR); | |
| if (reviews.length === 0) { | |
| console.log('⚠️ 현재 DOM에서 리뷰를 찾을 수 없습니다.'); | |
| return 0; | |
| } | |
| let addedCount = 0; | |
| reviews.forEach(review => { | |
| const id = review.id; | |
| if (!id || collectedReviewIds.has(id)) return; // 이미 수집한 리뷰는 스킵 | |
| ensureCollectorCapacity(); | |
| activeCollector.htmlChunks.push(review.outerHTML); | |
| activeCollector.count++; | |
| currentCollectorCount++; | |
| collectedReviewIds.add(id); | |
| addedCount++; | |
| }); | |
| updateStatusUI(); | |
| if (addedCount > 0) { | |
| console.log(`✅ ${addedCount}개의 새 리뷰를 수집 (누적 ${collectedReviewIds.size}개)`); | |
| } | |
| return addedCount; | |
| } | |
| function getTotalCollectedCount() { | |
| return collectors.reduce((sum, c) => sum + c.count, 0); | |
| } | |
| // ==================== 데이터 추출 함수 (콘솔용) ==================== | |
| function getCollectorHTML(index) { | |
| const collector = collectors.find(c => c.index === index); | |
| if (!collector) { | |
| console.log(`⚠️ 수집 영역 #${index}를 찾을 수 없습니다.`); | |
| return ''; | |
| } | |
| return collector.htmlChunks.join('\n'); | |
| } | |
| function getAllHTML() { | |
| return collectors.map(c => c.htmlChunks.join('\n')).join('\n'); | |
| } | |
| // ==================== 마운트/언마운트 ==================== | |
| function repositionMountedBoxes() { | |
| const panelHeight = statusDiv ? statusDiv.offsetHeight : 0; | |
| const panelBottom = 80; | |
| const boxHeight = 100; | |
| const gap = 10; | |
| let currentBottom = panelBottom + panelHeight + gap; | |
| const mountedBoxes = document.querySelectorAll('.review-collector-area'); | |
| mountedBoxes.forEach(box => { | |
| box.style.bottom = currentBottom + 'px'; | |
| currentBottom += boxHeight + gap; | |
| }); | |
| } | |
| function mountCollector(index) { | |
| const collector = collectors.find(c => c.index === index); | |
| if (!collector) { | |
| console.log(`⚠️ 수집 영역 #${index}를 찾을 수 없습니다.`); | |
| return null; | |
| } | |
| // 마운트 박스가 모달에 가려지지 않도록 모달 먼저 닫기 | |
| closeReviewModal(); | |
| const existingId = `review-collector-${index}`; | |
| const existing = document.getElementById(existingId); | |
| if (existing) { | |
| console.log(`ℹ️ 영역 #${index}는 이미 DOM에 있습니다.`); | |
| return existing; | |
| } | |
| const div = document.createElement('div'); | |
| div.id = existingId; | |
| div.className = 'review-collector-area'; | |
| const label = document.createElement('div'); | |
| label.className = 'rc-mount-label'; | |
| label.textContent = `📦 수집 영역 #${index} (${collector.count.toLocaleString()}개)`; | |
| div.appendChild(label); | |
| const guide = document.createElement('div'); | |
| guide.className = 'rc-mount-guide'; | |
| guide.textContent = 'Listly Parts 로 이 영역을 추출하세요'; | |
| div.appendChild(guide); | |
| const hidden = document.createElement('div'); | |
| hidden.style.display = 'none'; | |
| hidden.innerHTML = collector.htmlChunks.join('\n'); | |
| div.appendChild(hidden); | |
| document.body.appendChild(div); | |
| repositionMountedBoxes(); | |
| console.log(`✅ 영역 #${index} 데이터 불러오기 완료 (${collector.count}개) → #${existingId}`); | |
| return div; | |
| } | |
| function unmountCollector(index) { | |
| const div = document.getElementById(`review-collector-${index}`); | |
| if (div) { | |
| div.remove(); | |
| repositionMountedBoxes(); | |
| console.log(`🗑️ 영역 #${index} DOM에서 내리기 완료 (메모리 데이터 유지)`); | |
| return true; | |
| } | |
| console.log(`⚠️ 영역 #${index}가 DOM에 없습니다.`); | |
| return false; | |
| } | |
| // ==================== 메인 실행 함수 ==================== | |
| async function startReviewCollection() { | |
| console.log('🚀 리뷰 수집을 시작합니다... (모달 + 무한스크롤 모드)'); | |
| console.log(`📊 설정: 최대 스크롤 ${MAX_SCROLL_COUNT}회, 영역당 최대 ${MAX_REVIEWS_PER_COLLECTOR}개, 빈 스크롤 ${MAX_EMPTY_SCROLLS}회 시 종료`); | |
| initCollectors(); | |
| isCollecting = true; | |
| initStatusUI(); | |
| let totalReviews = getTotalCollectedCount(); | |
| if (totalReviews > 0) { | |
| console.log(`📦 기존 수집 리뷰: ${totalReviews}개`); | |
| } | |
| try { | |
| // 1단계: 모달 열기 | |
| const opened = await openReviewModal(); | |
| if (!opened) { | |
| console.log('❌ 모달을 열 수 없어 수집을 중단합니다.'); | |
| isCollecting = false; | |
| updateStatusUI(); | |
| return; | |
| } | |
| // 2단계: 최초 리뷰 수집 (모달 오픈 직후 보이는 리뷰) | |
| collectReviews(); | |
| // 3단계: 무한 스크롤로 계속 수집 | |
| let emptyScrollStreak = 0; | |
| let stagnantHeightStreak = 0; | |
| while (scrollCycleCount < MAX_SCROLL_COUNT) { | |
| scrollCycleCount++; | |
| console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); | |
| console.log(`📜 스크롤 ${scrollCycleCount}/${MAX_SCROLL_COUNT} 회`); | |
| const beforeCount = collectedReviewIds.size; | |
| const { heightGrew } = await scrollToBottomAndWait(); | |
| const addedCount = collectReviews(); | |
| const afterCount = collectedReviewIds.size; | |
| updateStatusUI(); | |
| // 종료 조건 체크 | |
| if (addedCount === 0) { | |
| emptyScrollStreak++; | |
| console.log(`ℹ️ 새 리뷰 없음 (${emptyScrollStreak}/${MAX_EMPTY_SCROLLS} 연속)`); | |
| } else { | |
| emptyScrollStreak = 0; | |
| } | |
| if (!heightGrew) { | |
| stagnantHeightStreak++; | |
| } else { | |
| stagnantHeightStreak = 0; | |
| } | |
| // 빈 스크롤이 연속되거나, 스크롤 높이가 안 늘어나는 상태가 충분히 이어지면 종료 | |
| if (emptyScrollStreak >= MAX_EMPTY_SCROLLS) { | |
| console.log(`✅ 연속 ${MAX_EMPTY_SCROLLS}회 새 리뷰가 없어 수집을 종료합니다.`); | |
| break; | |
| } | |
| if (stagnantHeightStreak >= MAX_EMPTY_SCROLLS && afterCount === beforeCount) { | |
| console.log(`✅ 더 이상 스크롤이 확장되지 않아 수집을 종료합니다.`); | |
| break; | |
| } | |
| } | |
| if (scrollCycleCount >= MAX_SCROLL_COUNT) { | |
| console.log(`⚠️ 최대 스크롤 횟수(${MAX_SCROLL_COUNT})에 도달했습니다.`); | |
| } | |
| isCollecting = false; | |
| updateStatusUI(); | |
| const finalTotal = getTotalCollectedCount(); | |
| console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); | |
| console.log(`✨ 수집 완료! 총 ${finalTotal}개 (고유 ID ${collectedReviewIds.size}개)`); | |
| console.log(`🖱️ 화면 오른쪽 하단 패널에서 영역별 "데이터 불러오기"/"내리기"를 조작하세요.`); | |
| } catch (error) { | |
| console.error('❌ 에러 발생:', error); | |
| isCollecting = false; | |
| updateStatusUI(); | |
| } | |
| } | |
| // ==================== 실행 ==================== | |
| startReviewCollection(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment