Skip to content

Instantly share code, notes, and snippets.

@listly-io
Created July 6, 2026 13:53
Show Gist options
  • Select an option

  • Save listly-io/b1fea63d727a8bd4ad65cd258570251f to your computer and use it in GitHub Desktop.

Select an option

Save listly-io/b1fea63d727a8bd4ad65cd258570251f to your computer and use it in GitHub Desktop.
ikea_review_2026-07-06
// 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