-
-
Save listly-io/25c96a75da8bafa1aae81fa8d21963d2 to your computer and use it in GitHub Desktop.
musinsa_review_2026-08-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.musinsa.com/review/goods/3098202?gf=A | |
| // ============================================================ | |
| // ▶ 사용자 설정 영역 (필요에 따라 수정하세요) | |
| // ============================================================ | |
| const MAX_SCROLL_COUNT = 100; // 최대 스크롤 횟수 (많을수록 더 많은 리뷰 수집) | |
| const STOP_AFTER_EMPTY = 5; // 새 리뷰가 안 나오는 스크롤이 연속 N회면 종료 | |
| const WAIT_AFTER_SCROLL = 1500; // 스크롤 후 로딩 대기 시간 (ms) | |
| const WAIT_BEFORE_SCROLL = 300; // 수집 후 스크롤 전 대기 시간 (ms) | |
| const REVIEW_LIST_SELECTOR = | |
| '.review-list-item__Container-sc-5kl1h0-0'; // 개별 리뷰 항목 셀렉터 | |
| const REVIEW_ID_ATTR = 'data-content-id'; // 리뷰 고유 ID 속성 (중복 제거용) | |
| // ※ 이 사이트(무신사)는 무한 스크롤 + DOM 재활용 방식이라 | |
| // 페이지네이션 버튼이 없습니다. 스크롤로 리뷰를 불러오며, | |
| // 화면 밖 리뷰는 DOM에서 제거되므로 REVIEW_ID_ATTR 로 중복을 걸러냅니다. | |
| const CONTAINER_ID = '__review_collector'; // 수집 컨테이너 ID | |
| // ============================================================ | |
| // ▶ 내부 로직 (수정 불필요) | |
| // ============================================================ | |
| (async () => { | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| // 수집 컨테이너 생성 | |
| 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}개`; | |
| }; | |
| // 이미 수집한 리뷰 ID 집합 (중복 제거용) | |
| const seen = new Set( | |
| [...store.children].map((c) => c.getAttribute(REVIEW_ID_ATTR)) | |
| ); | |
| // 현재 화면에 있는 리뷰 수집 (중복은 건너뜀) | |
| // 반환값 = 이번에 새로 추가된 리뷰 수 | |
| const collectCurrentView = () => { | |
| const reviews = document.querySelectorAll( | |
| `${REVIEW_LIST_SELECTOR}:not(#${CONTAINER_ID} *)` | |
| ); | |
| let added = 0; | |
| reviews.forEach((el) => { | |
| // 리뷰 고유 ID (없으면 앞부분 텍스트로 대체) | |
| const id = el.getAttribute(REVIEW_ID_ATTR) || el.innerText.slice(0, 50); | |
| if (seen.has(id)) return; | |
| seen.add(id); | |
| const clone = el.cloneNode(true); | |
| store.appendChild(clone); | |
| added++; | |
| }); | |
| console.log(`[Scraper] ${added}개 리뷰 수집 (누적 ${store.children.length}개)`); | |
| updateLabel(); | |
| return added; | |
| }; | |
| // 다음 리뷰 불러오기 (스크롤 → 대기) | |
| // ※ 이 사이트는 무한 스크롤이므로 버튼 클릭 대신 화면을 아래로 내립니다. | |
| const goNextScroll = async () => { | |
| await sleep(WAIT_BEFORE_SCROLL); | |
| window.scrollBy(0, window.innerHeight * 0.8); | |
| await sleep(WAIT_AFTER_SCROLL); | |
| }; | |
| // 메인 | |
| console.log(`[Scraper] 수집 시작 — 최대 ${MAX_SCROLL_COUNT}회 스크롤`); | |
| let emptyStreak = 0; | |
| for (let step = 1; step <= MAX_SCROLL_COUNT; step++) { | |
| // 현재 화면 수집 | |
| const added = collectCurrentView(); | |
| console.log(`[Scraper] ${step}/${MAX_SCROLL_COUNT} 스크롤 수집 완료`); | |
| // 새 리뷰가 없으면 연속 카운트, 있으면 초기화 | |
| if (added === 0) { | |
| emptyStreak++; | |
| } else { | |
| emptyStreak = 0; | |
| } | |
| // 연속으로 새 리뷰가 안 나오면 끝에 도달한 것으로 보고 종료 | |
| if (emptyStreak >= STOP_AFTER_EMPTY) { | |
| console.log('[Scraper] 더 이상 새 리뷰 없음 — 수집 종료'); | |
| break; | |
| } | |
| // 다음 리뷰 불러오기 위해 스크롤 | |
| await goNextScroll(); | |
| } | |
| collectCurrentView(); | |
| 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