-
-
Save listly-io/778081caa498828e64d8e489f492cbaa to your computer and use it in GitHub Desktop.
ohouse_review_2026-06-18
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://store.ohou.se/goods/3270412?affect_id=6&affect_type=StoreSearchResult | |
| // ============================================================ | |
| // ▶ 사용자 설정 영역 (필요에 따라 수정하세요) | |
| // ============================================================ | |
| const MAX_PAGE_COUNT = 10; // 수집할 페이지 수 (1 = 현재 페이지만) | |
| const WAIT_AFTER_CLICK = 5000; // 다음 페이지 클릭 후 대기 시간 (ms) | |
| const WAIT_BEFORE_CLICK = 1000; // 스크롤 후 클릭 전 대기 시간 (ms) | |
| const REVIEW_ITEM_SELECTOR = '.ew8r8221'; // 개별 리뷰 항목 셀렉터 (페이지당 5개) | |
| const PAGINATION_SELECTOR = '.e1o3bo0y0'; // 페이지네이션 영역 셀렉터 (페이지 내 2곳 존재) | |
| const NEXT_BTN_SELECTOR = 'button:last-child'; // 페이지네이션 내 다음(▶) 버튼 셀렉터 | |
| const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID | |
| // ============================================================ | |
| // ▶ 로직 | |
| // ============================================================ | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // ※ 이 사이트는 동일한 클래스의 페이지네이션이 2개(스타일링샷/리뷰) 존재하기 때문에 | |
| // 리뷰 항목을 실제로 포함하는 <section> 내부의 페이지네이션만 골라서 사용 | |
| // 리뷰 목록을 감싸는 section 반환 | |
| const getReviewSection = () => { | |
| const item = document.querySelector(REVIEW_ITEM_SELECTOR); | |
| if (!item) return null; | |
| let s = item; | |
| while (s && s.tagName !== 'SECTION') s = s.parentElement; | |
| return s; | |
| }; | |
| // 리뷰 전용 페이지네이션 영역 반환 | |
| const getPagination = () => { | |
| const sec = getReviewSection(); | |
| return sec ? sec.querySelector(PAGINATION_SELECTOR) : null; | |
| }; | |
| // 리뷰 전용 "다음" 버튼 반환 | |
| const getNextBtn = () => { | |
| const pag = getPagination(); | |
| return pag ? pag.querySelector(NEXT_BTN_SELECTOR) : null; | |
| }; | |
| // 1. 수집 컨테이너 생성 (이미 있으면 재사용) | |
| 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}개`; | |
| }; | |
| // 2. 현재 페이지 리뷰 수집 함수 | |
| const collectCurrentPage = () => { | |
| const reviews = document.querySelectorAll( | |
| `${REVIEW_ITEM_SELECTOR}:not(#${CONTAINER_ID} *)` | |
| ); | |
| let added = 0; | |
| reviews.forEach((el) => { | |
| const clone = el.cloneNode(true); | |
| store.appendChild(clone); | |
| added++; | |
| }); | |
| console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${store.children.length}개)`); | |
| updateLabel(); | |
| }; | |
| // 3. 다음 페이지 버튼 클릭 함수 (스크롤 → 대기 → 클릭) | |
| // 반환: true = 클릭 성공, false = 버튼 없음/비활성 | |
| const goNextPage = async () => { | |
| const btn = getNextBtn(); | |
| if (!btn || btn.disabled) return false; | |
| // 페이지네이션 영역으로 스크롤 | |
| const paginationArea = getPagination(); | |
| if (paginationArea) { | |
| paginationArea.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| await sleep(WAIT_BEFORE_CLICK); | |
| // 다시 한번 버튼 상태 확인 (스크롤 중 상태 변경 가능) | |
| const btnAgain = getNextBtn(); | |
| if (!btnAgain || btnAgain.disabled) return false; | |
| btnAgain.click(); | |
| return true; | |
| }; | |
| // 4. 메인 | |
| 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을 꺼내려면 콘솔에서 아래 영역 참고 | |
| // document.getElementById('__review_store').innerHTML | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment