-
-
Save listly-io/b1fea63d727a8bd4ad65cd258570251f to your computer and use it in GitHub Desktop.
ikea_review_2026-07-06
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
| // test page: https://www.ikea.com/us/en/p/skogsta-dining-table-acacia-black-70419264/ | |
| // ============================================================ | |
| // ▶ 사용자 설정 영역 (필요에 따라 수정하세요) | |
| // ============================================================ | |
| const MAX_PAGE_COUNT = 10; // '더 보기' 클릭 반복 횟수 (1 = 현재 로드된 리뷰) | |
| const WAIT_AFTER_CLICK = 3000; // '더 보기' 클릭 후 대기 시간 (ms) | |
| const WAIT_BEFORE_CLICK = 1000; // 스크롤 후 클릭 전 대기 시간 (ms) | |
| // ※ IKEA는 PowerReviews 위젯(모달)을 사용하며, 페이지가 교체되는 방식이 아니라 | |
| // 같은 리스트에 리뷰가 계속 추가되는 "Load more" 방식 | |
| const REVIEW_LIST_SELECTOR = '.ugc-rr-pip-fe-reviews-container .ugc-rr-pip-fe-review__wrapper'; // 개별 리뷰 항목 셀렉터 | |
| const PAGINATION_SELECTOR = '.ugc-rr-pip-fe-reviews__load-more'; // '더 보기' 버튼 영역 셀렉터 | |
| const NEXT_BTN_SELECTOR = `${PAGINATION_SELECTOR} button:last-child`; // '더 보기' 버튼 셀렉터 | |
| const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID | |
| const SCRAPED_MARK_ATTR = 'data-__scraped'; // 이미 수집한 리뷰 표시 속성 (append 방식 중복 방지용) | |
| const OPEN_REVIEWS_BTN_TEXT = 'Show all reviews'; // 리뷰 모달을 여는 버튼 텍스트 | |
| // ============================================================ | |
| // ▶ 로직 | |
| // ============================================================ | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // 리뷰 목록이 실제로 렌더링될 때까지 최대 10초 대기 | |
| const ensureReviewsOpen = async () => { | |
| if (document.querySelector(REVIEW_LIST_SELECTOR)) return true; | |
| const btn = Array.from(document.querySelectorAll('button')).find( | |
| (b) => b.offsetParent !== null && b.textContent.trim().toLowerCase() === OPEN_REVIEWS_BTN_TEXT.toLowerCase() | |
| ); | |
| if (!btn) { | |
| console.log('[Scraper] "Show all reviews" 버튼을 찾을 수 없음'); | |
| return false; | |
| } | |
| btn.click(); | |
| for (let i = 0; i < 20; i++) { | |
| await sleep(500); | |
| if (document.querySelector(REVIEW_LIST_SELECTOR)) return true; | |
| } | |
| return false; | |
| }; | |
| // 수집 컨테이너 생성 (이미 있으면 재사용) | |
| let container = document.getElementById(CONTAINER_ID); | |
| if (!container) { | |
| container = document.createElement('div'); | |
| container.id = CONTAINER_ID; | |
| Object.assign(container.style, { | |
| position: 'fixed', | |
| right: '200px', | |
| bottom: '200px', | |
| zIndex: '999999', | |
| background: 'rgba(0, 0, 0, 0.85)', | |
| color: '#0f0', | |
| fontSize: '18px', | |
| fontWeight: 'bold', | |
| fontFamily: 'monospace', | |
| padding: '16px 24px', | |
| borderRadius: '12px', | |
| border: '2px solid #0f0', | |
| boxShadow: '0 4px 20px rgba(0,255,0,.3)', | |
| cursor: 'default', | |
| userSelect: 'none', | |
| overflow: 'hidden', | |
| }); | |
| document.body.appendChild(container); | |
| } | |
| // 추출 저장 영역 (숨김 영역) — 컨테이너 내부에 display:none 인 래퍼 | |
| let store = container.querySelector('#__review_store'); | |
| if (!store) { | |
| store = document.createElement('div'); | |
| store.id = '__review_store'; | |
| store.style.display = 'none'; | |
| container.appendChild(store); | |
| } | |
| // 카운트 표시 라벨 | |
| let label = container.querySelector('#__review_label'); | |
| if (!label) { | |
| label = document.createElement('div'); | |
| label.id = '__review_label'; | |
| container.appendChild(label); | |
| } | |
| // ▶ 수집된 실제 리뷰 개수 (하드코딩 offset 제거 — store에 담긴 수 그대로 사용) | |
| const displayCount = () => store.children.length; | |
| const updateLabel = () => { | |
| label.textContent = `수집된 리뷰: ${displayCount()}개`; | |
| }; | |
| updateLabel(); | |
| // 현재 로드된 리뷰 추출 함수 | |
| // IKEA는 '더 보기' 클릭 시 기존 리뷰가 사라지지 않고 새 리뷰가 추가되므로 | |
| // 이미 수집한 요소를 SCRAPED_MARK_ATTR로 표시해 중복 수집을 방지 | |
| const collectCurrentPage = () => { | |
| const reviews = document.querySelectorAll( | |
| `${REVIEW_LIST_SELECTOR}:not([${SCRAPED_MARK_ATTR}]):not(#${CONTAINER_ID} *)` | |
| ); | |
| let added = 0; | |
| reviews.forEach((el) => { | |
| el.setAttribute(SCRAPED_MARK_ATTR, 'true'); | |
| const clone = el.cloneNode(true); | |
| store.appendChild(clone); | |
| added++; | |
| }); | |
| console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${displayCount()}개)`); | |
| updateLabel(); | |
| }; | |
| // 다음 페이지("더 보기") 버튼 클릭 함수 (스크롤 → 대기 → 클릭) | |
| // 반환: true = 클릭 성공, false = 버튼 없음/비활성 | |
| const goNextPage = async () => { | |
| const btn = document.querySelector(NEXT_BTN_SELECTOR); | |
| if (!btn || btn.disabled) return false; | |
| const paginationArea = document.querySelector(PAGINATION_SELECTOR); | |
| if (paginationArea) { | |
| paginationArea.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| await sleep(WAIT_BEFORE_CLICK); | |
| const btnAgain = document.querySelector(NEXT_BTN_SELECTOR); | |
| if (!btnAgain || btnAgain.disabled) return false; | |
| btnAgain.click(); | |
| return true; | |
| }; | |
| // 메인 | |
| const opened = await ensureReviewsOpen(); | |
| if (!opened) { | |
| console.log('[Scraper] 리뷰 모달을 열지 못했습니다 — 수집 종료'); | |
| return; | |
| } | |
| console.log(`[Scraper] 수집 시작 — 최대 ${MAX_PAGE_COUNT}회 '더 보기'`); | |
| for (let page = 1; page <= MAX_PAGE_COUNT; page++) { | |
| collectCurrentPage(); | |
| console.log(`[Scraper] ${page}/${MAX_PAGE_COUNT} 회차 수집 완료`); | |
| if (page === MAX_PAGE_COUNT) break; | |
| const moved = await goNextPage(); | |
| if (!moved) { | |
| console.log('[Scraper] "더 보기" 버튼 없음 — 수집 종료'); | |
| break; | |
| } | |
| await sleep(WAIT_AFTER_CLICK); | |
| } | |
| console.log(`[Scraper] 수집 완료! 총 ${displayCount()}개 리뷰`); | |
| updateLabel(); | |
| // ▶ 향후 수집된 HTML을 꺼내려면 콘솔에서 아래 영역 참고: | |
| // document.getElementById('__review_store').innerHTML | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment