-
-
Save listly-io/9b9781d82aedb7bade5a10b1f22e848a to your computer and use it in GitHub Desktop.
oliveyoung_review_2025-11-27
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
| // ========== 설정값 ========== | |
| const MAX_ITEM_COUNT = 100; // 수집 목표 개수 | |
| const SCROLL_AMOUNT = 500; // 스크롤 px | |
| const SCROLL_DELAY = 1000; // 스크롤 후 대기 시간 (ms) | |
| const ITEM_SELECTOR = 'oy-review-review-item'; | |
| const DATE_SELECTOR = 'span.date'; | |
| const NAME_SELECTOR = 'div.name'; | |
| const CONTENT_SELECTOR = 'div.content'; | |
| // ============================ | |
| // 수집된 아이템을 저장할 Set (중복 방지) | |
| const collectedItems = new Map(); | |
| // Shadow DOM을 재귀적으로 탐색하여 특정 셀렉터를 찾는 함수 | |
| function findInShadowDOM(root, selector) { | |
| // 현재 레벨에서 찾기 | |
| let elements = root.querySelectorAll(selector); | |
| if (elements.length > 0) { | |
| return Array.from(elements); | |
| } | |
| // Shadow root가 있는 모든 요소 탐색 | |
| const allElements = root.querySelectorAll('*'); | |
| let found = []; | |
| for (let element of allElements) { | |
| if (element.shadowRoot) { | |
| const result = findInShadowDOM(element.shadowRoot, selector); | |
| found = found.concat(result); | |
| } | |
| } | |
| return found; | |
| } | |
| // 특정 요소 내부에서 셀렉터를 찾는 함수 (Shadow DOM 포함) | |
| function findInElement(element, selector) { | |
| // 먼저 일반 DOM에서 찾기 | |
| let result = element.querySelector(selector); | |
| if (result) return result; | |
| // Shadow root 탐색 | |
| if (element.shadowRoot) { | |
| result = findInShadowDOM(element.shadowRoot, selector); | |
| if (result.length > 0) return result[0]; | |
| } | |
| // 자식 요소들의 shadow root 탐색 | |
| const children = element.querySelectorAll('*'); | |
| for (let child of children) { | |
| if (child.shadowRoot) { | |
| result = findInShadowDOM(child.shadowRoot, selector); | |
| if (result.length > 0) return result[0]; | |
| } | |
| } | |
| return null; | |
| } | |
| // 아이템에서 데이터 추출 | |
| function extractItemData(item) { | |
| const dateElement = findInElement(item, DATE_SELECTOR); | |
| const nameElement = findInElement(item, NAME_SELECTOR); | |
| const contentElement = findInElement(item, CONTENT_SELECTOR); | |
| return { | |
| date: dateElement ? dateElement.textContent.trim() : '', | |
| name: nameElement ? nameElement.textContent.trim() : '', | |
| content: contentElement ? contentElement.textContent.trim() : '' | |
| }; | |
| } | |
| // 고유 ID 생성 (날짜 + 이름 + 내용 일부) | |
| function generateItemId(data) { | |
| return `${data.date}_${data.name}_${data.content.substring(0, 50)}`; | |
| } | |
| // 현재 페이지의 아이템 수집 | |
| function collectCurrentItems() { | |
| const items = findInShadowDOM(document.body, ITEM_SELECTOR); | |
| let newCount = 0; | |
| items.forEach(item => { | |
| const data = extractItemData(item); | |
| const id = generateItemId(data); | |
| if (!collectedItems.has(id) && data.date && data.name && data.content) { | |
| collectedItems.set(id, data); | |
| newCount++; | |
| } | |
| }); | |
| return newCount; | |
| } | |
| // 상태 표시 div 생성 | |
| function createStatusDiv() { | |
| const statusDiv = document.createElement('div'); | |
| statusDiv.id = 'review-collector-status'; | |
| statusDiv.style.cssText = ` | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| font-size: 14px; | |
| z-index: 10000; | |
| min-width: 200px; | |
| `; | |
| document.body.appendChild(statusDiv); | |
| return statusDiv; | |
| } | |
| // 상태 업데이트 | |
| function updateStatus(statusDiv, message) { | |
| statusDiv.innerHTML = ` | |
| <div><strong>리뷰 수집기</strong></div> | |
| <div>수집된 아이템: ${collectedItems.size} / ${MAX_ITEM_COUNT}</div> | |
| <div>${message}</div> | |
| `; | |
| } | |
| // 수집된 아이템을 div에 표시 | |
| function displayCollectedItems(statusDiv) { | |
| statusDiv.innerHTML = ` | |
| <div><strong>수집 완료!</strong></div> | |
| <div>총 ${collectedItems.size}개 수집</div> | |
| `; | |
| collectedItems.forEach((data, id) => { | |
| const itemDiv = document.createElement('div'); | |
| itemDiv.style.display = 'none'; | |
| itemDiv.className = 'collected-review-item'; | |
| const dateSpan = document.createElement('span'); | |
| dateSpan.className = 'review-date'; | |
| dateSpan.textContent = data.date; | |
| const nameSpan = document.createElement('span'); | |
| nameSpan.className = 'review-name'; | |
| nameSpan.textContent = data.name; | |
| const contentDiv = document.createElement('div'); | |
| contentDiv.className = 'review-content'; | |
| contentDiv.textContent = data.content; | |
| itemDiv.appendChild(dateSpan); | |
| itemDiv.appendChild(nameSpan); | |
| itemDiv.appendChild(contentDiv); | |
| statusDiv.appendChild(itemDiv); | |
| }); | |
| } | |
| // 메인 수집 함수 | |
| async function startCollection() { | |
| const statusDiv = createStatusDiv(); | |
| updateStatus(statusDiv, '수집 시작...'); | |
| // 초기 수집 | |
| collectCurrentItems(); | |
| updateStatus(statusDiv, '스크롤 중...'); | |
| while (collectedItems.size < MAX_ITEM_COUNT) { | |
| // 스크롤 | |
| window.scrollBy(0, SCROLL_AMOUNT); | |
| updateStatus(statusDiv, '스크롤 중...'); | |
| // 대기 | |
| await new Promise(resolve => setTimeout(resolve, SCROLL_DELAY)); | |
| // 새 아이템 수집 | |
| const newCount = collectCurrentItems(); | |
| updateStatus(statusDiv, `새 아이템 ${newCount}개 발견`); | |
| // 더 이상 스크롤할 곳이 없는지 체크 | |
| if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) { | |
| console.log('페이지 끝에 도달했습니다.'); | |
| break; | |
| } | |
| } | |
| // 결과 표시 | |
| displayCollectedItems(statusDiv); | |
| console.log('수집 완료:', collectedItems); | |
| return Array.from(collectedItems.values()); | |
| } | |
| // 실행 | |
| startCollection(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment