{
  "version": "1.0.0",
  "exported_at": "2026-06-01T04:45:00.000Z",
  "project": {
    "name": "Naver Map Review Scraper",
    "description": "Scrapes Naver Map / Naver Place shop visitor reviews for store URL(s), using 왕거미식당 place id 20601144 from the Octoparse listing. The workflow opens the mobile visitor-review page, scrolls/clicks more/expand controls, normalizes detected review DOM or embedded JSON into hidden export rows, then appends Octoparse-compatible columns to naver-map-review-scraper.csv. If Naver blocks access, shows CAPTCHA, or changes the DOM/API, the template outputs a diagnostic fallback row instead of failing. Add more direct Naver Place visitor-review URLs to navigate.urls for multiple stores.",
    "color": "bg-[#03c75a]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 100,
      "position_y": 240,
      "config": {
        "width": 430,
        "height": 932,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 460,
      "position_y": 240,
      "config": {
        "urls": [
          "https://m.place.naver.com/restaurant/20601144/review/visitor?entry=plt"
        ],
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 820,
      "position_y": 240,
      "config": {
        "timeout": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1180,
      "position_y": 240,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1540,
      "position_y": 240,
      "config": {
        "duration": 3,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript on page",
      "position_x": 1900,
      "position_y": 240,
      "config": {
        "jsCode": "return (async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  const clean = (s) => (s || '').replace(/\\s+/g, ' ').trim();\n  const textOf = (el) => clean(el?.innerText || el?.textContent || '');\n  const abs = (href) => {\n    try { return href ? new URL(href, location.href).href : ''; } catch (e) { return href || ''; }\n  };\n\n  const clickByText = async (regex, limit = 12) => {\n    const nodes = Array.from(document.querySelectorAll('button, a, [role=\"button\"]')).filter((el) => regex.test(textOf(el)));\n    let clicked = 0;\n    for (const el of nodes.slice(0, limit)) {\n      try {\n        el.scrollIntoView({ block: 'center', inline: 'center' });\n        await sleep(120);\n        el.click();\n        clicked += 1;\n        await sleep(350);\n      } catch (e) {}\n    }\n    return clicked;\n  };\n\n  for (let i = 0; i < 55; i++) {\n    await clickByText(/더보기|리뷰\\s*더보기|펼쳐보기|전체보기|more/i, 8);\n    window.scrollTo(0, document.body.scrollHeight);\n    await sleep(800);\n    const loadingText = textOf(document.body);\n    if (/captcha|자동입력|보안문자|비정상적인 접근/i.test(loadingText)) break;\n  }\n  await clickByText(/더보기|펼쳐보기|전체보기|more/i, 20);\n  await sleep(700);\n\n  const bodyText = textOf(document.body);\n  const metaTitle = document.querySelector('meta[property=\"og:title\"], meta[name=\"title\"]')?.content || document.title || '';\n  const ogDesc = document.querySelector('meta[property=\"og:description\"], meta[name=\"description\"]')?.content || '';\n  const pageText = clean(bodyText + ' ' + ogDesc + ' ' + metaTitle);\n\n  const storeName = clean(\n    document.querySelector('h1')?.innerText ||\n    document.querySelector('[class*=\"GHAhO\"], [class*=\"Fc1rA\"], [class*=\"place_title\"], [class*=\"title\"]')?.innerText ||\n    metaTitle.replace(/\\s*[:|-]?\\s*네이버.*$/, '') ||\n    '왕거미식당'\n  );\n\n  const category = clean(\n    document.querySelector('[class*=\"lnJFt\"], [class*=\"DJJvD\"], [class*=\"place_category\"], [class*=\"category\"]')?.innerText ||\n    ((pageText.match(/\\b(한식|카페|디저트|중식|일식|양식|분식|음식점|술집|베이커리|고기|육류|회|해산물)\\b/) || [])[1] || '')\n  );\n\n  const rating = clean((pageText.match(/(?:별점|평점)\\s*([0-9]+(?:\\.[0-9]+)?)/) || [])[1] || '');\n  const visitReviewCount = clean((pageText.match(/방문자리뷰\\s*([0-9,]+)/) || [])[1] || '');\n  const blogReviewCount = clean((pageText.match(/블로그리뷰\\s*([0-9,]+)/) || [])[1] || '');\n\n  const detectReviewNode = (el) => {\n    if (!el || el === document.body || el === document.documentElement) return false;\n    const txt = textOf(el);\n    if (txt.length < 15 || txt.length > 5000) return false;\n    const cls = String(el.className || '');\n    const hasReviewImg = !!el.querySelector('img[src*=\"pup-review\"], img[src*=\"search.pstatic\"], img[src*=\"review\"], img[alt*=\"리뷰\"]');\n    const hasMyLink = !!el.querySelector('a[href*=\"/my/\"], a[href*=\"m.place.naver.com/my\"]');\n    const hasVisit = /\\d+번째\\s*방문|영수증|방문인증|네이버예약|예약|주문/.test(txt);\n    const hasDate = /20\\d{2}년\\s*\\d{1,2}월\\s*\\d{1,2}일/.test(txt);\n    const hasGood = /이런 점이 좋았어요|음식이 맛있어요|친절해요|매장이 넓어요|빨리 나와요|재료가 신선해요|가성비/.test(txt);\n    const pui = /pui__|review|visitor|place/i.test(cls);\n    return hasReviewImg || hasMyLink || hasVisit || hasGood || (pui && hasDate);\n  };\n\n  let nodes = Array.from(document.querySelectorAll('li, article, section, div')).filter(detectReviewNode);\n  nodes = nodes.filter((el) => !Array.from(el.children).some((child) => detectReviewNode(child) && textOf(child).length > 20));\n\n  const seen = new Set();\n  const reviews = [];\n\n  const addReview = (r) => {\n    const key = clean([r.reviewer, r.content, r.visitTime, r.img, r.rawText].join('|')).slice(0, 500);\n    if (!key || seen.has(key)) return;\n    seen.add(key);\n    reviews.push(r);\n  };\n\n  for (const row of nodes) {\n    const raw = textOf(row);\n    if (!raw) continue;\n\n    const img = row.querySelector('img[src*=\"pup-review\"], img[src*=\"search.pstatic\"], img[src*=\"review\"], img[alt*=\"리뷰\"], img')?.src || '';\n    const myLinkEl = row.querySelector('a[href*=\"/my/\"], a[href*=\"m.place.naver.com/my\"]');\n    const reviewerLink = abs(myLinkEl?.getAttribute('href') || '');\n\n    const reviewerCandidates = [\n      myLinkEl?.innerText,\n      row.querySelector('[class*=\"NMi\"], [class*=\"nickname\"], [class*=\"user\"], [class*=\"User\"], [class*=\"name\"], [class*=\"Name\"]')?.innerText\n    ].map(clean).filter(Boolean);\n\n    let reviewer = reviewerCandidates[0] || '';\n    if (!reviewer) {\n      const firstLine = clean((row.innerText || '').split('\\n').map(clean).find((x) => x && !/방문|리뷰|영수증|더보기|접기|사진|20\\d{2}/.test(x)) || '');\n      reviewer = firstLine.length <= 40 ? firstLine : '';\n    }\n\n    const reviewCount = clean((raw.match(/리뷰\\s*([0-9,]+)/) || [])[1] || '');\n    const followerCount = clean((raw.match(/팔로워\\s*([0-9,]+)/) || [])[1] || '');\n    const visitTime = clean((raw.match(/20\\d{2}년\\s*\\d{1,2}월\\s*\\d{1,2}일(?:\\s*[월화수목금토일]요일)?/) || [])[0] || '');\n    const visitCount = clean((raw.match(/\\d+번째\\s*방문/) || [])[0] || '');\n    const visitVerification = clean((raw.match(/영수증|방문인증|예약|주문|네이버예약/) || [])[0] || '');\n    const goodPointMatches = raw.match(/[^.!?\\n]{0,28}(?:좋아요|맛있어요|친절해요|넓어요|빨리 나와요|청결해요|특별해요|가성비가 좋아요|분위기가 좋아요|재료가 신선해요|뷰가 좋아요|주차하기 편해요|아늑해요|양이 많아요)/g) || [];\n    const goodPoints = clean(Array.from(new Set(goodPointMatches.map(clean))).join(' | '));\n\n    let content = raw;\n    const removeBits = [storeName, category, rating, visitReviewCount, blogReviewCount, reviewer, reviewCount, followerCount, visitTime, visitCount, visitVerification, '이런 점이 좋았어요', '더보기', '접기', '방문자리뷰', '블로그리뷰'];\n    for (const bit of removeBits) {\n      if (bit) content = content.split(bit).join(' ');\n    }\n    for (const gp of goodPointMatches) {\n      if (gp) content = content.split(gp).join(' ');\n    }\n    content = clean(content);\n\n    if (content.length > 1800) content = content.slice(0, 1800);\n    if (!content && !goodPoints && !visitTime && !img) continue;\n\n    addReview({\n      storeName,\n      category,\n      rating,\n      visitReviewCount,\n      blogReviewCount,\n      reviewer,\n      reviewerLink,\n      reviewCount,\n      followerCount,\n      img,\n      content,\n      goodPoints,\n      visitTime,\n      visitCount,\n      visitVerification,\n      sourceUrl: location.href,\n      rawText: raw.slice(0, 2500)\n    });\n  }\n\n  const scriptTexts = Array.from(document.scripts).map((s) => s.textContent || '').join('\\n');\n  const embedded = scriptTexts.match(/.{0,120}(?:reviewBody|reviewText|contents|visitorReview|nickname|receipt|별점|영수증).{0,500}/gi) || [];\n  for (const chunk of embedded.slice(0, 60)) {\n    const raw = clean(chunk.replace(/[{}\\[\\]\"\\\\]/g, ' '));\n    if (raw.length < 30) continue;\n    const visitTime = clean((raw.match(/20\\d{2}년\\s*\\d{1,2}월\\s*\\d{1,2}일(?:\\s*[월화수목금토일]요일)?/) || [])[0] || '');\n    const img = clean((raw.match(/https?:\\/\\/[^\\s,]+(?:pup-review|search\\.pstatic)[^\\s,]+/) || [])[0] || '');\n    addReview({\n      storeName,\n      category,\n      rating,\n      visitReviewCount,\n      blogReviewCount,\n      reviewer: '',\n      reviewerLink: '',\n      reviewCount: '',\n      followerCount: '',\n      img,\n      content: raw.slice(0, 1200),\n      goodPoints: '',\n      visitTime,\n      visitCount: clean((raw.match(/\\d+번째\\s*방문/) || [])[0] || ''),\n      visitVerification: clean((raw.match(/영수증|방문인증|예약|주문|네이버예약/) || [])[0] || ''),\n      sourceUrl: location.href,\n      rawText: raw.slice(0, 2500)\n    });\n  }\n\n  if (reviews.length === 0) {\n    const blocked = /captcha|자동입력|보안문자|비정상적인 접근|로그인|권한|접근이 제한/i.test(pageText) ? 'possible_block_or_login_required' : 'no_review_rows_detected';\n    reviews.push({\n      storeName,\n      category,\n      rating,\n      visitReviewCount,\n      blogReviewCount,\n      reviewer: '',\n      reviewerLink: '',\n      reviewCount: '',\n      followerCount: '',\n      img: document.querySelector('meta[property=\"og:image\"]')?.content || '',\n      content: blocked,\n      goodPoints: '',\n      visitTime: '',\n      visitCount: '',\n      visitVerification: '',\n      sourceUrl: location.href,\n      rawText: pageText.slice(0, 2500)\n    });\n  }\n\n  let container = document.querySelector('#uscraper-naver-reviews');\n  if (container) container.remove();\n  container = document.createElement('div');\n  container.id = 'uscraper-naver-reviews';\n  container.style.display = 'none';\n\n  const set = (el, name, value) => el.setAttribute(name, value || '');\n  for (const r of reviews) {\n    const div = document.createElement('div');\n    div.className = 'uscraper-review-row';\n    set(div, 'data-store-name', r.storeName);\n    set(div, 'data-category', r.category);\n    set(div, 'data-rating', r.rating);\n    set(div, 'data-visit-review-count', r.visitReviewCount);\n    set(div, 'data-blog-review-count', r.blogReviewCount);\n    set(div, 'data-reviewer', r.reviewer);\n    set(div, 'data-reviewer-link', r.reviewerLink);\n    set(div, 'data-review-count', r.reviewCount);\n    set(div, 'data-follower-count', r.followerCount);\n    set(div, 'data-image', r.img);\n    set(div, 'data-review-content', r.content);\n    set(div, 'data-good-points', r.goodPoints);\n    set(div, 'data-visit-time', r.visitTime);\n    set(div, 'data-visit-count', r.visitCount);\n    set(div, 'data-visit-verification', r.visitVerification);\n    set(div, 'data-source-url', r.sourceUrl);\n    set(div, 'data-raw-text', r.rawText);\n    container.appendChild(div);\n  }\n  document.body.appendChild(container);\n  return reviews.length;\n})();",
        "waitForCompletion": true,
        "timeout": 150,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2260,
      "position_y": 240,
      "config": {
        "selector": "#uscraper-naver-reviews .uscraper-review-row",
        "timeout": 20,
        "visible": false,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2620,
      "position_y": 240,
      "config": {
        "rowSelector": "#uscraper-naver-reviews .uscraper-review-row",
        "fileName": "naver-map-review-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "가게이름",
            "selector": "",
            "attribute": "data-store-name"
          },
          {
            "name": "카테고리",
            "selector": "",
            "attribute": "data-category"
          },
          {
            "name": "전체평점",
            "selector": "",
            "attribute": "data-rating"
          },
          {
            "name": "방문자리뷰",
            "selector": "",
            "attribute": "data-visit-review-count"
          },
          {
            "name": "블로그리뷰",
            "selector": "",
            "attribute": "data-blog-review-count"
          },
          {
            "name": "리뷰작성자",
            "selector": "",
            "attribute": "data-reviewer"
          },
          {
            "name": "리뷰작성자링크",
            "selector": "",
            "attribute": "data-reviewer-link"
          },
          {
            "name": "리뷰작성수",
            "selector": "",
            "attribute": "data-review-count"
          },
          {
            "name": "팔로워",
            "selector": "",
            "attribute": "data-follower-count"
          },
          {
            "name": "첨부이미지",
            "selector": "",
            "attribute": "data-image"
          },
          {
            "name": "리뷰내용",
            "selector": "",
            "attribute": "data-review-content"
          },
          {
            "name": "이런_점이_좋아요",
            "selector": "",
            "attribute": "data-good-points"
          },
          {
            "name": "방문시간",
            "selector": "",
            "attribute": "data-visit-time"
          },
          {
            "name": "방분횟수",
            "selector": "",
            "attribute": "data-visit-count"
          },
          {
            "name": "방문인증",
            "selector": "",
            "attribute": "data-visit-verification"
          },
          {
            "name": "source_url",
            "selector": "",
            "attribute": "data-source-url"
          },
          {
            "name": "raw_review_text",
            "selector": "",
            "attribute": "data-raw-text"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2980,
      "position_y": 240,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 28,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 388,
      "position_y": 136,
      "width": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1828,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2548,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2908,
      "position_y": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Naver Map / Naver Place shop visitor reviews for store URL(s), using 왕거미식당 place id 20601144 from the Octoparse listing. The workflow opens the mobile visitor-review page, scrolls/clicks more/expand controls, normalizes detected review DOM or embedded JSON into hidden export rows, then appends Octoparse-compatible columns to naver-map-review-scraper.csv. If Naver blocks access, shows CAPTCHA, or changes the DOM/API, the template outputs a diagnostic fallback row instead of failing. Add more direct Naver Place visitor-review URLs to navigate.urls for multiple stores.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 1 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 660,
      "position_y": 220,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (async () => {\n  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\n  c...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2100,
      "position_y": 220,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Extracts rows matching `#uscraper-naver-reviews .uscraper-review-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2820,
      "position_y": 220,
      "width": 340,
      "height": 119,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 3180,
      "position_y": 220,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}