시리즈 목차 (4편 연재)
1/4. 구매대행 크롬 확장 — 프로그램 개념 잡기
2/4. 구매대행 크롬 확장 — Svelte 5·MV3 사이드패널 만들기
3/4. 구매대행 크롬 확장 — 1688 주문·상품 수집과 엑셀 자동화 (현재 글)
4/4. 구매대행에 붙이는 관리자 페이지 — 환경설정·통관검증·엑셀 병합
1편에서 구매대행 확장의 세 기둥과 Shadow DOM·접힌 주문 개념을, 2편에서 Side Panel·메시지·빌드 골격을 다뤘습니다. 이번 글에서는 실무 업무 데이터 — 1688 주문 스크래핑부터 배송대행용 엑셀까지 — 를 집중적으로 설명합니다.
개인적으로 사용하기 위해 만든 것이고, 많은 분들이 궁금해 하는 부분이기도 해서 도움이 되면 좋겠습니다.
대상 페이지가 아니면 사이드패널 도구가 비활성으로 표시된 화면이 보입니다.


예제 코드: 시리즈 4편 첨부
chromeExtExample.zip(로그인·관심상품 API 없음)
아래 코드 인용 경로는 zip 기준(chromeExtExample/…)입니다. 확장 구조·빌드는 2편을 참고하세요.
1. 이 글에서 다루는 데이터
공유 예제의 핵심은 주문 행 → 배송대행용 엑셀 입니다.
1.1. 주문 행 (Purchase Order Item)
- 출처: 1688 주문 목록 페이지 (
/app/ctf-page/trade-order-list/buyer-order-list.html) - 구조:
<order-item>커스텀 엘리먼트 (Shadow DOM 포함) - 목적: 주문번호, 상품명, 색상, 가격, 수량, 트래킹번호 등을 Excel로 정리
- UI: 사이드패널 구매 도구 (
PurchaseTools)
관심상품 수집·백엔드 API 저장은 zip에 포함하지 않았고, 이 글에서도 다루지 않습니다.
2. 1688 주문 목록 DOM·Shadow DOM 수집
2.1. 접힌 주문 펼치기 (필수 선행 작업)
1688 구매자 주문 목록은 주문이 많을 때 상품 행을 접어 둔 상태로 보여 줍니다. 접혀 있으면 DOM(및 Shadow DOM)에 상세 라인 아이템이 없거나 불완전해서, 확장이 상품명·색상·가격·수량·트래킹번호 등을 제대로 수집할 수 없습니다.
따라서 구매내역 추출 / 분석엑셀 생성 전에 반드시 주문을 펼쳐야 합니다. 사이드패널 PurchaseTools의 버튼으로 일괄 처리합니다.
| 버튼 | 동작 |
|---|---|
| Active 버턴확인 | 페이지에서 「펼치기」 대상 버튼 개수를 센다 (COUNT_TARGET_BUTTONS) |
| Active 버턴실행 | 찾은 버튼을 클릭해 접힌 주문을 펼친다 (CLICK_TARGET_BUTTON) |
실제 1688 주문 목록 규칙에서는 캡션이 「주문정보 모두 보이기」 이며, 대상 셀렉터는 예를 들어 다음과 같습니다.
btnCaption: '주문정보 모두 보이기',
buttonSelectors: ['button[data-role="submit-order"]', 'button.submit']
권장 순서:
- 1688 구매자 주문 목록 페이지로 이동한다.
- Active 버턴확인으로 펼치기 버튼이 몇 개인지 확인한다.
- Active 버턴실행으로 모두 펼친다. (버튼이 남아 있으면 한 번 더 실행)
- 그다음 구매내역 추출 + 데이터 보기 또는 분석엑셀 생성을 진행한다.
주문을 펼치지 않고 추출하면 행이 누락되거나 빈 값이 많아질 수 있습니다. 주문이 많은 날일수록 이 단계가 중요합니다.
2.2. <order-item> 커스텀 엘리먼트
1688 주문 목록은 Web Components로 구현되어 있으며, 각 주문은 <order-item> 안에 캡슐화됩니다. Shadow DOM을 순회해야 내부 정보를 추출할 수 있습니다.
2.3. queryAllDeep - Shadow DOM 순회
const queryAllDeep = (selector) => {
const results = new Set();
const visit = (root) => {
let matches;
try {
matches = root.querySelectorAll(selector);
} catch (error) {
console.warn('[content] querySelectorAll 실패:', selector, error);
matches = [];
}
matches.forEach((node) => results.add(node));
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
let current = walker.nextNode();
while (current) {
if (current instanceof Element && current.shadowRoot) {
visit(current.shadowRoot);
}
current = walker.nextNode();
}
};
visit(document);
return Array.from(results);
};
흐름:
document.querySelectorAll(selector)실행TreeWalker로 모든 엘리먼트를 순회하며shadowRoot확인- Shadow DOM이 있으면 재귀적으로
visit(current.shadowRoot)호출 - 모든 매칭 노드를
Set에 수집해 중복 제거 후 반환
2.4. collectOrderItems - 주문 목록 수집
function collectOrderItems() {
const containers = queryAllDeep('.order-list-content');
const orderItems = queryAllDeep('order-item');
if (!orderItems.length) return null;
let bestContainer = null;
/** @type {Element[]} */
let bestItems = [];
if (containers.length) {
const containerSet = new Set(containers);
const containerToItems = new Map();
orderItems.forEach((item) => {
let current = item;
while (current) {
const parent = current.parentNode;
if (!parent) break;
if (parent instanceof ShadowRoot) {
current = parent.host;
continue;
}
if (containerSet.has(parent)) {
const list = containerToItems.get(parent) ?? [];
list.push(item);
containerToItems.set(parent, list);
break;
}
current = parent;
}
});
containerToItems.forEach((items, container) => {
if (items.length > bestItems.length) {
bestItems = items;
bestContainer = container;
}
});
}
if (!bestItems.length) {
bestItems = orderItems;
bestContainer = containers[0] ?? document.body;
}
return [bestContainer, bestItems];
}
흐름:
.order-list-content컨테이너와order-item엘리먼트 모두 수집- 각
order-item이 어느 컨테이너에 속하는지 부모 노드를 따라 추적 - 가장 많은 item을 포함한 컨테이너를
bestContainer로 선택 [bestContainer, bestItems]반환 — 이후 파싱·엑셀 행 생성에 사용
zip의 Active 버튼 처리도 같은 queryAllDeep를 씁니다 (handleSelectorAction, COUNT_TARGET_BUTTONS / CLICK_TARGET_BUTTON).
3. 주문 그룹 → Excel 행 매핑과 날짜 필터
3.1. 정규식 패턴 기반 데이터 추출 (개념)
실무에서는 workOption.parsingConfigs의 정규식으로 Shadow DOM HTML에서 필드를 뽑습니다. 공유 zip의 content.js는 이 매핑 전체를 넣지 않았고, 파싱 골격만 담았습니다. 개념상 패턴은 예를 들면 다음과 같습니다.
// chromeExtExample — 정규식 매핑은 zip 축약본에 미포함 (개념 예시)
regexPatterns: [
{ name: '주문그룹', pattern: /<order-item[^>]*>([\s\S]*?)<\/order-item>/gi },
{ name: '쇼핑몰 오더번호', pattern: /<span class="order-id">[\s\S]*?text="([^"]+)"/gi, group: 1 },
{ name: '트래킹번호', pattern: /class="ap-pkg-quick-view-details__title"[^>]*>[\s\S]*?([A-Z0-9-]{6,})<\/div>/gi, group: 1 },
{ name: '상품명', pattern: /<a class="product-name"[^>]*>[\s\S]*?<!--\?lit\$[^$]*\$-->([^<]+)<\/a>/gi, group: 1 },
{ name: '색상', pattern: /颜色:\s*[\s\S]*?<!--\?lit\$[^$]*\$-->([^<]+)/gi, group: 1 },
{ name: '가격', pattern: /<div class="actual-unit-price">¥[\s\S]*?<!--\?lit\$[^$]*\$-->([^<]+)/gi, group: 1 },
{ name: '수량', pattern: /<span class="quantity-amount">[\s\S]*?<!--\?lit\$[^$]*\$-->([^<]+)/gi, group: 1 }
]
// excelHeaders 예: 국내쇼핑몰주문번호, 상품명, 색상, 사이즈, HS CODE, 트래킹번호 …
주요 필드:
- 주문그룹:
<order-item>전체 HTML (그룹 단위로 처리) - 쇼핑몰 오더번호:
<span class="order-id">의text="..."속성 - 상품명, 색상, 가격, 수량: Lit 템플릿 주석(
<!--?lit$...$-->) 다음 텍스트 - 트래킹번호: Shadow DOM 내 물류 타이틀 근처 영문·숫자 6자 이상
- 쇼핑몰 주문일: DOM에서 직접 추출하는 경우가 많음 (정규식만으로 부족할 때)
3.2. 날짜 필터링
사이드패널에서 시작일/종료일을 넘기면, content script의 PARSE_1688_PURCHASES가 이를 받아 주문일 기준으로 거릅니다. zip 골격은 필터 값을 로그로만 남깁니다.
const parseAndSummarize = async (opts = {}) => {
const pair = collectOrderItems();
if (!pair) {
return { ok: false, error: 'order-item 을 찾지 못했습니다. 접힌 주문을 먼저 펼치세요.' };
}
const [, orderItems] = pair;
emitDebug(`order-item ${orderItems.length}개 (날짜필터: ${opts.filterDateStart}~${opts.filterDateEnd})`);
/** @type {Record<string, string>[]} */
const excelRows = orderItems.map((item, index) => ({
상품명: `(예제) 상품 ${index + 1} — 원본은 정규식으로 Shadow DOM 텍스트 추출`,
'쇼핑몰 오더번호': '',
트래킹번호: '',
__detailLink: item.querySelector?.('a.order-detail-action')?.href || ''
}));
await fillMissingTrackingNumbers(excelRows);
return {
ok: true,
totalCount: excelRows.length,
excelRows,
headers: ['상품명', '쇼핑몰 오더번호', '트래킹번호'],
note: '공유용 골격입니다. 실무 매핑은 원본 content.js 의 parsingConfigs 를 보세요.'
};
};
실무에서는 filterDateStart / filterDateEnd로 각 order-item의 주문일을 비교해 범위 밖을 제외한 뒤 엑셀 행을 만듭니다.
3.3. Excel 행 생성
zip 골격은 위처럼 placeholder 행을 만들고 fillMissingTrackingNumbers까지 호출합니다. 실무에서는 excelHeaders 순서대로 값을 채우고 convertHsCode 같은 변환 함수를 적용합니다.
사이드패널에서 받은 excelRows / headers는 Background의 DOWNLOAD_EXCEL로 넘깁니다.
if (message?.type === 'DOWNLOAD_EXCEL') {
(async () => {
try {
const { rows, headers, filename } = message.payload ?? {};
const blob = createExcelBlob(rows, headers);
const dataUrl = (await blobToDataUrl(blob)) ?? '';
chrome.downloads.download(
{
url: dataUrl,
filename: `${filename || 'venti_orders'}.xls`,
saveAs: true
},
(downloadId) => {
if (chrome.runtime.lastError) {
const errorMessage = chrome.runtime.lastError.message;
sendResponse({ ok: false, error: errorMessage });
return;
}
sendResponse({ ok: true, downloadId });
}
);
} catch (error) {
console.error('[background] DOWNLOAD_EXCEL 실패:', error);
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
}
})();
return true;
}
4. 누락 운송장 번호를 상세 페이지에서 보완
주문 목록 페이지에서는 트래킹번호가 없거나 Shadow DOM 구조 변경으로 추출에 실패할 수 있습니다. 이 경우 백그라운드 탭으로 상세 페이지를 열어 다시 추출합니다.
4.1. Background에서 백그라운드 탭 생성
if (message?.type === 'FETCH_TRACKING_NUMBER') {
(async () => {
try {
const { url } = message.payload ?? {};
if (!url) {
sendResponse({ ok: false, error: '상세 URL이 제공되지 않았습니다.' });
return;
}
const trackingNumber = await extractTrackingNumberFromDetail(url);
sendResponse({ ok: true, trackingNumber });
} catch (error) {
console.error('[background] FETCH_TRACKING_NUMBER 실패:', error);
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
}
})();
return true;
}
핵심 구현은 zip의 extractTrackingNumberFromDetail 입니다.
const extractTrackingNumberFromDetail = async (url) => {
let tabId;
try {
const tab = await createTab({ url });
tabId = tab.id;
await waitForTabComplete(tabId);
const trackingNumber = await executeDetailExtraction(tabId);
return trackingNumber;
} finally {
await removeTab(tabId);
}
};
executeDetailExtraction(chromeExtExample/static/background.js) 안에서는:
chrome.tabs.create({ active: false })로 백그라운드 탭 생성- 로딩 완료 대기 후
chrome.scripting.executeScript실행 - 「物流信息」 탭 클릭 →
.info-item중 「运单号码」 값 추출 - 트래킹번호 반환 후 탭 제거
4.2. Content Script에서 호출
const fetchTrackingNumberFromDetail = async (url) => {
if (!url) return null;
const response = await new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'FETCH_TRACKING_NUMBER', payload: { url } }, (res) => {
if (chrome.runtime.lastError) {
resolve({ ok: false, error: chrome.runtime.lastError.message });
return;
}
resolve(res ?? { ok: false });
});
});
return response?.ok && response.trackingNumber ? response.trackingNumber : null;
};
/**
* 목록에 없는 트래킹번호를 상세 페이지(백그라운드 탭)에서 보완
* @param {Record<string, string>[]} rows
* @returns {Promise<void>}
*/
const fillMissingTrackingNumbers = async (rows) => {
if (!rows?.length) return;
for (const row of rows) {
if (row['트래킹번호']?.trim()) continue;
const detailUrl = row.__detailLink || state.detailLinks[0];
if (!detailUrl) continue;
const trackingNumber = await fetchTrackingNumberFromDetail(detailUrl);
if (trackingNumber) row['트래킹번호'] = trackingNumber;
delete row.__detailLink;
}
};
흐름:
- 트래킹번호가 비어 있는 행만 대상
__detailLink(또는 수집해 둔 상세 URL)로 Background에 요청- 받아 온 운송장 번호를 행에 채움
실무에서는 같은 주문번호 행을 묶어 한 번만 상세를 열고, 그룹 전체에 같은 트래킹을 넣을 수 있습니다.
5. 환경설정(ventigoods.agency)으로 HS CODE·통관번호 정규화
실무에서는 동일한 상품명이 반복되므로, 환경설정 텍스트로 HS CODE, 통관번호, 브랜드, 배송지상품명, 색상별 이미지 URL 등을 자동 매핑합니다.
공유 zip에는 매핑용 샘플 파일을 넣었고, content 쪽 복호화·변환 함수 전체는 골격(RELOAD_WORK_OPTION no-op)만 있습니다.
5.1. localStorage 샘플 (# Text 형식)
키는 ventigoods.agency. zip 샘플 전체는 chromeExtExample/samples/ventigoods.agency.sample.txt 입니다.
# Text
# Ventigoods product info
# localStorage 키: ventigoods.agency
# 사이드패널 환경설정(또는 DevTools → Application → Local Storage)에 그대로 붙여 넣으면 됩니다.
# (# Encoded 형태는 암호화본 — 공유 예제에서는 # Text 만 다룹니다.)
이어서 한 줄짜리 ENV_VENTI_PRODUCT_INFO={…} 와 ENV_DUMMY={…} 가 붙습니다.ENV_VENTI_PRODUCT_INFO JSON 안의 주요 맵:
| 키 | 역할 |
|---|---|
productImageUrl |
1688 상품명 → 색상/SKU → 타배용 이미지 URL |
color |
중국어 색상명 → 한글 표기 |
brand |
상품명 → 브랜드 |
productName |
상품명 → 배송지(영문 등) 상품명 |
hsCode |
상품명 → HS CODE |
ccNumber |
상품명 → 통관품목번호 |
additionService |
상품명 → 부가서비스 코드(쉼표 구분) |
키가 1688 주문에서 추출한 상품명 문자열과 정확히 일치해야 매핑됩니다.
5.2. 코드 쪽 골격
if (message?.type === 'RELOAD_WORK_OPTION') {
// 원본: workOption.init() — localStorage ventigoods.agency 재로드
emitDebug('RELOAD_WORK_OPTION (예제: no-op)');
sendResponse({ ok: true });
return true;
}
개념상 흐름:
// 1) localStorage['ventigoods.agency'] 읽기 (# Text / # Encoded)
// 2) ENV_VENTI_PRODUCT_INFO.* 맵에서 상품명(·색상)으로 조회
// 3) excelHeaders.func (convertHsCode, convertCcNumber …) 로 행 값 치환
convertHsCode(value, rowData) {
const hsCodeMap = workOptionData?.ENV_VENTI_PRODUCT_INFO?.hsCode;
return hsCodeMap?.[rowData['상품명']] ?? value;
}
예시 (샘플 기준):
hsCode['亚马逊手摇太阳能充电收音机 …']→"852712"productImageUrl[상품명]['橙色']→ 타배 이미지 URL
6. DOM 스크래핑 운영 시 주의점
6.1. 접힌 주문은 반드시 펼친 뒤 추출
주문이 많으면 1688이 라인 아이템을 접어 둡니다. Active 버턴확인 → Active 버턴실행으로 「주문정보 모두 보이기」를 처리한 뒤에만 추출·엑셀 생성을 하세요. 접힌 상태에서는 수집 누락이 발생합니다.
6.2. Shadow DOM 변경 대응
1688은 Web Components 기반이므로, 셀렉터가 자주 바뀝니다. queryAllDeep 같은 재귀 순회 패턴을 사용하면 구조 변경에 유연하게 대응할 수 있습니다.
6.3. 정규식 패턴 유지보수
1688 HTML 구조가 바뀌면 정규식·셀렉터를 고쳐야 합니다. 공유 zip은 골격만 있으므로, 실무에선 환경설정(또는 코드의 parsingConfigs)을 유지보수합니다.
7. 시리즈 다시 보기
이 글로 1688 주문 수집부터 배송대행용 엑셀까지를 마칩니다. 앞에서 쓴 골격·개념은 아래를 참고하세요.
👉 1편: 구매대행 크롬 확장 — 프로그램 개념 잡기
👉 2편: 구매대행 크롬 확장 — Svelte 5·MV3 사이드패널 만들기
쿠팡 주문과의 병합·통관번호 검증·ENV 관리자 UI는 4편에서 이어서 다룹니다.
👉 다음 편 예고: 구매대행에 붙이는 관리자 페이지 — 환경설정·통관검증·엑셀 병합
추출·정규화가 끝나면 이런 형태의 엑셀을 받게 됩니다.

사이드패널에서 분석엑셀 생성 / 다운로드 하면 아래처럼 저장할 수 있습니다.

참고 자료
이 글이 도움이 되었다면, 실무 데이터 자동화에 Chrome 확장을 활용해 보세요. Shadow DOM과 백그라운드 탭 활용은 복잡한 웹페이지에서도 안정적으로 데이터를 수집하는 핵심 패턴입니다.


주파수 소통방 (0)
로딩 중...