Skip to content

Instantly share code, notes, and snippets.

@listly-io
Created July 17, 2026 02:50
Show Gist options
  • Select an option

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

Select an option

Save listly-io/38f99fa91352f9f03c9e69a1ea60b36a to your computer and use it in GitHub Desktop.
wayfair_review_2026-07-16
// test page: https://www.wayfair.com/furniture/pdp/lark-manor-tyler-nailhead-swivel-barrel-chair-w113055765.html
//
// [주의] Wayfair 웹 사이트의 경우, 첫 페이지 이후의 페이지들에서 확인할 수 있는 리뷰들은 웹 사이트 로그인을 후 접근 가능합니다.
// 그렇기 때문에 해당 웹 사이트에 먼저 로그인을 한 후 이 스크립트를 사용하시면 문제 없이 원하는 페이지에서 리뷰를 추출하실 수 있습니다.
//
// [사용법]
// 0. 오픈코드 사용법 참고 (https://app.gitbook.com/o/Uu6AM3fgpZza0EpsJM4N/s/iKxoYELFf9WS8XCvYe6S/support/open-codes/how-to)
// 1. 상품 리뷰 페이지에서 개발자 도구를 열어 콘솔에 아래 스크립트를 붙여넣기 → 자동 수집 시작
// 2. 수집 완료 후, "수집된 리뷰: N" 박스 영역을 Listly Parts로 선택하여 추출
//
// ============================================================
// ▶ 사용자 설정 영역 (필요에 따라 수정해주세요)
// ============================================================
const MAX_PAGE_COUNT = 10; // 수집할 페이지 수 (1 = 현재 페이지 / "다음" 클릭 횟수 + 1)
const WAIT_AFTER_CLICK = 5000; // 다음 페이지("다음") 클릭 후 대기 시간 (ms)
const WAIT_BEFORE_CLICK = 1000; // 스크롤 후 클릭 전 대기 시간 (ms)
const REVIEW_TAB_SELECTOR = 'h2'; // 리뷰 탭 버튼 셀렉터 (텍스트 "리뷰" 로 식별)
const REVIEW_LIST_SELECTOR = '._6o3atz1ay'; // 개별 리뷰 항목 셀렉터
const PAGINATION_SELECTOR = 'button'; // 페이지네이션(다음 버튼이 속한) 영역 셀렉터
const NEXT_BTN_TEXT = 'Show More Reviews'; // 다음("다음") 버튼 식별 텍스트
const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID
// ======================== 데이터 추출 ====================================
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 리뷰 탭 활성화
// 탭이 비활성 상태면 .review-items 가 렌더링되지 않으므로, 먼저 탭을 클릭
const ensureReviewTab = async () => {
const tabs = Array.from(document.querySelectorAll(REVIEW_TAB_SELECTOR));
const reviewTab = tabs.find((t) => /리뷰|Customer Reviews/i.test(t.innerText));
if (reviewTab) {
reviewTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// 리뷰 카드가 실제로 렌더링될 때까지 대기 (최대 10초)
for (let i = 0; i < 20; i++) {
if (document.querySelector(REVIEW_LIST_SELECTOR)) break;
await sleep(500);
}
};
await ensureReviewTab();
// 수집 컨테이너 생성
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);
}
const updateLabel = () => {
const count = store.children.length;
label.textContent = `수집된 리뷰: ${count}개`;
};
// 중복 방지용 키 집합 (Wayfair는 "Show More Reviews" 클릭 시 리뷰가 추가되므로,
// 라이브 엘리먼트의 data-속성 대신 리뷰 내용 기반으로 중복을 판별함)
const seen = new Set();
const reviewKey = (el) => {
// 작성자 + 날짜 + 본문 앞부분을 조합해 고유 키 생성
const writer = (el.querySelector('span._6o3atzbl')?.innerText || '').trim();
const dateEl = Array.from(el.querySelectorAll('span')).find((s) => /^\d{2}\/\d{2}\/\d{4}$/.test(s.innerText.trim()));
const date = (dateEl?.innerText || '').trim();
const desc = (el.querySelector('p._6o3atzbl')?.innerText || '').trim();
return `${writer}|${date}|${desc}`.slice(0, 200) || el.innerText.trim().slice(0, 200);
};
// 현재 페이지 리뷰 수집 함수
// (Wayfair는 "Show More Reviews" 클릭 시 기존 리스트에 추가되므로, 내용 기반으로 이미 수집한 항목은 건너뜀)
const collectCurrentPage = () => {
const reviews = Array.from(
document.querySelectorAll(`${REVIEW_LIST_SELECTOR}:not(#${CONTAINER_ID} *)`)
).filter((el) => /Rated \d out of 5/.test(el.innerText));
let added = 0;
reviews.forEach((el) => {
const key = reviewKey(el);
if (seen.has(key)) return; // 이미 수집한 리뷰는 건너뜀
seen.add(key);
const clone = el.cloneNode(true);
store.appendChild(clone);
added++;
});
console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${store.children.length}개)`);
updateLabel();
};
// 다음 페이지("다음") 버튼 클릭 함수
// 반환: true = 클릭 성공, false = 버튼 없음/비활성
const findNextBtn = () =>
Array.from(document.querySelectorAll(PAGINATION_SELECTOR)).find((b) =>
new RegExp(NEXT_BTN_TEXT, 'i').test(b.textContent)
);
const goNextPage = async () => {
const btn = findNextBtn();
if (!btn || btn.disabled || btn.classList.contains('disabled') || btn.offsetParent === null) return false;
// 페이지네이션("다음" 버튼) 영역으로 스크롤
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(WAIT_BEFORE_CLICK);
// 다시 한번 버튼 상태 확인 (스크롤 중 상태 변경 가능)
const btnAgain = findNextBtn();
if (!btnAgain || btnAgain.disabled || btnAgain.classList.contains('disabled') || btnAgain.offsetParent === null) return false;
btnAgain.click();
return true;
};
// 메인
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] 수집 완료! 총 ${store.children.length}개 리뷰`);
updateLabel();
// ▶ 추후 수집된 HTML을 꺼내려면 콘솔에서 아래 ID 확인
// document.getElementById('__review_store').innerHTML
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment