{
  "version": "1.0.0",
  "exported_at": "2026-06-03T10:25:00.000Z",
  "project": {
    "name": "Coupang Review Scrapercloud",
    "description": "Best-effort Coupang product review scraper equivalent to Octoparse Coupang Review Scraper(cloud). Coupang returned Access Denied in live testing, so real review rows could not be accessed from this browser/IP/session. The template navigates Coupang product URLs, then attempts same-origin XHR review pagination for pages 1-25 and normalizes any returned review HTML into rows. If Coupang blocks the page and endpoints, it outputs one diagnostic row with blocked_or_page_status rather than silently producing no file. Navigation strategy: product URL list via navigate.urls[] + loop-continue; internal review pagination is handled by injected JavaScript.",
    "color": "bg-[#4589ff]",
    "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": 120,
      "position_y": 260,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 260,
      "config": {
        "urls": [
          "https://www.coupang.com/vp/products/8420658733"
        ],
        "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": 840,
      "position_y": 260,
      "config": {
        "timeout": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1200,
      "position_y": 260,
      "config": {
        "jsCode": "(() => {\n  const old = document.querySelector('#uscraper-reviews');\n  if (old) old.remove();\n  const out = document.createElement('div');\n  out.id = 'uscraper-reviews';\n  out.style.display = 'none';\n  document.body.appendChild(out);\n\n  const dec = s => {\n    const t = document.createElement('textarea');\n    t.innerHTML = s || '';\n    return t.value;\n  };\n  const clean = s => (s || '').replace(/\\s+/g, ' ').trim();\n  const firstText = (root, selectors) => {\n    for (const sel of selectors) {\n      const el = root.querySelector(sel);\n      if (el && clean(el.textContent)) return clean(el.textContent);\n    }\n    return '';\n  };\n\n  const productId = (location.href.match(/products\\/(\\d+)/) || location.href.match(/productId=(\\d+)/) || [])[1] || '8420658733';\n  const productUrl = 'https://www.coupang.com/vp/products/' + productId;\n  const currentBody = document.body ? (document.body.innerText || '') : '';\n  const pageBlocked = /Access Denied|permission to access|errors\\.edgesuite|captcha|CAPTCHA/i.test(currentBody);\n\n  const productNameFromPage = pageBlocked ? '' : firstText(document, [\n    'h2.prod-buy-header__title',\n    '.prod-buy-header__title',\n    '[class*=\"ProductTitle\"]',\n    '[class*=\"product-title\"]',\n    '[class*=\"prod-title\"]',\n    'h1'\n  ]);\n\n  const reviewCountFromPage = (() => {\n    if (pageBlocked) return '';\n    const candidates = [\n      firstText(document, ['.prod-review__count', '.sdp-review__average__total-star__info-count', '[class*=\"review-count\"]']),\n      currentBody.match(/(?:상품평|리뷰|review)\\s*[(:]?\\s*([0-9,]{2,})/i)?.[1] || ''\n    ].filter(Boolean).join(' ');\n    const m = candidates.replace(/,/g, '').match(/\\d{2,}/);\n    return m ? m[0] : '';\n  })();\n\n  const priceFromPage = pageBlocked ? '' : firstText(document, ['.prod-sale-price .total-price strong', '.prod-price .total-price strong', '.total-price strong']);\n  const unitPriceFromPage = pageBlocked ? '' : firstText(document, ['.prod-sale-price .unit-price', '.prod-price .unit-price', '.unit-price']);\n  const wowPriceFromPage = pageBlocked ? '' : firstText(document, ['.prod-coupon-price .total-price strong', '.prod-promotion-price .total-price strong', '[class*=\"wow\"] strong']);\n  const wowUnitPriceFromPage = pageBlocked ? '' : firstText(document, ['.prod-coupon-price .unit-price', '.prod-promotion-price .unit-price', '[class*=\"wow\"] .unit-price']);\n  const overallRatingFromPage = (() => {\n    if (pageBlocked) return '';\n    const t = firstText(document, ['.prod-review__rating .rating-average', '.sdp-review__average__total-star__info .value', '[class*=\"rating-average\"]']);\n    const m = t.match(/\\d+(?:\\.\\d+)?/);\n    return m ? m[0] : '';\n  })();\n\n  const rowSelector = '.sdp-review__article__list, li[class*=review], div[class*=review-item], div[class*=ReviewItem], article[class*=review]';\n  const seen = new Set();\n  let lastStatus = '';\n  let responsePreview = '';\n\n  const addRow = (r, sourcePage) => {\n    const rawText = clean(r.innerText || r.textContent || '');\n    if (rawText.length < 8) return;\n    const key = rawText.slice(0, 260);\n    if (seen.has(key)) return;\n    seen.add(key);\n\n    const option = firstText(r, [\n      '.sdp-review__article__list__info__product-info__name',\n      '[class*=product-info__name]',\n      '[class*=ProductInfo]',\n      '[class*=option]',\n      '[class*=Option]'\n    ]);\n    const user = firstText(r, [\n      '.sdp-review__article__list__info__user__name',\n      '[class*=user__name]',\n      '[class*=UserName]',\n      '[class*=nickname]',\n      '[class*=reviewer]'\n    ]) || rawText.split('\\n').map(clean).filter(Boolean)[0] || '';\n    const date = firstText(r, [\n      '.sdp-review__article__list__info__product-info__reg-date',\n      '[class*=reg-date]',\n      '[class*=date]',\n      '[class*=Date]',\n      'time'\n    ]) || ((rawText.match(/\\d{4}[.년\\/-]\\s*\\d{1,2}[.월\\/-]\\s*\\d{1,2}/) || [''])[0]);\n    const title = firstText(r, [\n      '.sdp-review__article__list__headline',\n      '[class*=headline]',\n      '[class*=title]',\n      '[class*=Title]'\n    ]);\n    const content = firstText(r, [\n      '.sdp-review__article__list__review__content',\n      '[class*=review__content]',\n      '[class*=content]',\n      '[class*=Content]',\n      'p'\n    ]) || rawText;\n\n    const star = r.querySelector('.sdp-review__article__list__info__product-info__star-orange,[class*=star-orange],[aria-label*=점],[aria-label*=star],[class*=rating],[class*=Rating]');\n    let rating = '';\n    if (star) {\n      const style = star.getAttribute('style') || '';\n      const pct = style.match(/width\\s*:\\s*(\\d+(?:\\.\\d+)?)%/);\n      if (pct) rating = String(Math.round(parseFloat(pct[1]) / 20));\n      if (!rating) rating = (((star.textContent || star.getAttribute('aria-label') || star.getAttribute('title') || '').match(/\\d+(?:\\.\\d+)?/) || [''])[0]);\n    }\n    const photos = Array.from(r.querySelectorAll('img'))\n      .map(img => img.currentSrc || img.src || img.getAttribute('data-src') || img.getAttribute('data-original') || '')\n      .filter(src => src && !src.startsWith('data:'))\n      .join(' | ');\n\n    const el = document.createElement('div');\n    el.className = 'uscraper-review-row';\n    Object.assign(el.dataset, {\n      productUrl,\n      product: productNameFromPage || (option.includes(',') ? option.split(',')[0].trim() : ''),\n      reviewCount: reviewCountFromPage,\n      coupangPrice: priceFromPage.replace(/[^0-9,]/g, ''),\n      coupangUnitPrice: unitPriceFromPage,\n      wowPrice: wowPriceFromPage.replace(/[^0-9,]/g, ''),\n      wowUnitPrice: wowUnitPriceFromPage,\n      overallRating: overallRatingFromPage,\n      user,\n      rating,\n      date: clean(date),\n      option,\n      title,\n      content,\n      photos,\n      status: 'review_row_from_' + sourcePage\n    });\n    out.appendChild(el);\n  };\n\n  Array.from(document.querySelectorAll(rowSelector)).forEach(r => addRow(r, 'visible_dom'));\n\n  for (let page = 1; page <= 25; page++) {\n    try {\n      const url = `/vp/product/reviews?productId=${encodeURIComponent(productId)}&page=${page}&size=100&sortBy=ORDER_SCORE_ASC&ratings=&q=&viRoleCode=3&ratingSummary=true`;\n      const xhr = new XMLHttpRequest();\n      xhr.open('GET', url, false);\n      xhr.setRequestHeader('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');\n      xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');\n      xhr.send(null);\n      lastStatus = String(xhr.status);\n      const txt = xhr.responseText || '';\n      if (!responsePreview && txt) responsePreview = clean(txt.slice(0, 700));\n      if (!txt || /Access Denied|permission to access|errors\\.edgesuite|captcha|CAPTCHA/i.test(txt)) continue;\n      const variants = [txt, dec(txt)];\n      try {\n        const j = JSON.parse(txt);\n        const walk = x => {\n          if (typeof x === 'string') {\n            if (/sdp-review|PRODUCTREVIEW|review/i.test(x)) variants.push(dec(x));\n          } else if (x && typeof x === 'object') {\n            Object.values(x).forEach(walk);\n          }\n        };\n        walk(j);\n      } catch (e) {}\n      let pageRows = 0;\n      variants.forEach(html => {\n        const doc = new DOMParser().parseFromString(html, 'text/html');\n        const rows = Array.from(doc.querySelectorAll(rowSelector));\n        pageRows += rows.length;\n        rows.forEach(r => addRow(r, 'review_endpoint_page_' + page));\n      });\n      if (page > 1 && pageRows === 0) break;\n    } catch (e) {\n      lastStatus = 'xhr_error_' + (e && e.message ? e.message : 'unknown');\n    }\n  }\n\n  if (out.children.length === 0) {\n    const el = document.createElement('div');\n    el.className = 'uscraper-review-row uscraper-diagnostic-row';\n    Object.assign(el.dataset, {\n      productUrl,\n      product: productNameFromPage,\n      reviewCount: reviewCountFromPage,\n      coupangPrice: pageBlocked ? '' : priceFromPage.replace(/[^0-9,]/g, ''),\n      coupangUnitPrice: pageBlocked ? '' : unitPriceFromPage,\n      wowPrice: pageBlocked ? '' : wowPriceFromPage.replace(/[^0-9,]/g, ''),\n      wowUnitPrice: pageBlocked ? '' : wowUnitPriceFromPage,\n      overallRating: pageBlocked ? '' : overallRatingFromPage,\n      user: '',\n      rating: '',\n      date: '',\n      option: '',\n      title: '',\n      content: pageBlocked ? 'Coupang returned Access Denied in this browser session. Real review rows were not accessible.' : ('No review rows found. Last endpoint status: ' + lastStatus + '. Preview: ' + responsePreview),\n      photos: '',\n      status: pageBlocked ? 'blocked_or_access_denied' : 'no_review_rows_or_endpoint_blocked'\n    });\n    out.appendChild(el);\n  }\n\n  return out.children.length;\n})()",
        "waitForCompletion": true,
        "timeout": 60,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 260,
      "config": {
        "duration": 1,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 260,
      "config": {
        "selector": "#uscraper-reviews .uscraper-review-row",
        "timeout": 10,
        "visible": false,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 260,
      "config": {
        "rowSelector": "#uscraper-reviews .uscraper-review-row",
        "fileName": "coupang-review-scraper-cloud.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "상품_URL",
            "selector": "ROW.dataset.productUrl || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "상품",
            "selector": "ROW.dataset.product || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "상품평_개수__개_",
            "selector": "ROW.dataset.reviewCount || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "쿠팡판매가",
            "selector": "ROW.dataset.coupangPrice || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "단가_쿠팡_",
            "selector": "ROW.dataset.coupangUnitPrice || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "와우할인가",
            "selector": "ROW.dataset.wowPrice || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "단가_와우_",
            "selector": "ROW.dataset.wowUnitPrice || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "전체_평점",
            "selector": "ROW.dataset.overallRating || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "고객_아이디",
            "selector": "ROW.dataset.user || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "고객_평점",
            "selector": "ROW.dataset.rating || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "리뷰_작성_시간",
            "selector": "ROW.dataset.date || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "구매상품",
            "selector": "ROW.dataset.option || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "리뷰_제목",
            "selector": "ROW.dataset.title || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "상세리뷰",
            "selector": "ROW.dataset.content || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "포토리뷰",
            "selector": "ROW.dataset.photos || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "blocked_or_page_status",
            "selector": "ROW.dataset.status || ''",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 260,
      "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": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-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": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "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": 48,
      "position_y": 156,
      "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": 408,
      "position_y": 156,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1128,
      "position_y": 156,
      "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": 2208,
      "position_y": 156,
      "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": 2568,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Coupang product review scraper equivalent to Octoparse Coupang Review Scraper(cloud). Coupang returned Access Denied in live testing, so real review rows could not be accessed from this browser/IP/session. The template navigates Coupang product URLs, then attempts same-origin XHR review pagination for pages 1-25 and normalizes any returned review HTML into rows. If Coupang blocks the page and endpoints, it outputs one diagnostic row with blocked_or_page_status rather than silently producing no file. Navigation strategy: product URL list via navigate.urls[] + loop-continue; internal review pagination is handled by injected JavaScript.",
      "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": 680,
      "position_y": 240,
      "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: `(() => {\n  const old = document.querySelector('#uscraper-reviews');\n  if (old) old.remove();\n  const...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1400,
      "position_y": 240,
      "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": "Structured export with JS columns (상품_URL, 상품, 상품평_개수__개_, 쿠팡판매가, 단가_쿠팡_). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 240,
      "width": 340,
      "height": 126,
      "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": 2840,
      "position_y": 240,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}