/** ===== 설정 =====
* 1) 실행 전 '스크립트 속성'에 아래 키를 저장하세요.
* NAVER_CLIENT_ID, NAVER_CLIENT_SECRET
* (앱스스클: 상단 톱니바 > 프로젝트 설정 > 스크립트 속성)
*/
const PROP = PropertiesService.getScriptProperties();
const CLIENT_ID = PROP.getProperty('NAVER_CLIENT_ID');
const CLIENT_SECRET = PROP.getProperty('NAVER_CLIENT_SECRET');
const API_URL = 'https://openapi.naver.com/v1/search/shop.json';
const MAX_TOTAL = 1000;
const MAX_PER_REQUEST = 100;
/** 웹앱 진입 (index.html 렌더) */
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Naver Shopping Crawler');
}
/** 공용 유틸: 태그/엔티티 정리 */
function cleanText(s) {
if (!s) return '';
// <b> 태그 및 기타 태그 제거
s = s.replace(/<\/?b>/gi, '').replace(/<[^>]+>/g, '');
// HTML 엔티티 디코딩
return s.replace(/&/g,"&")
.replace(/</g,"<")
.replace(/>/g,">")
.replace(/"/g,'"')
.replace(/'/g,"'");
}
/** 공용 유틸: 영문/숫자/언더스코어 슬러그 */
function slugifyAscii(text) {
return (text || 'query')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
}
/** 핵심: 네이버 쇼핑 검색 + 시트 쓰기 */
function fetchShoppingToSheet(query, totalCount) {
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error('스크립트 속성에 NAVER_CLIENT_ID / NAVER_CLIENT_SECRET를 설정하세요.');
}
if (!query) throw new Error('검색 키워드는 비어 있을 수 없습니다.');
totalCount = Math.max(1, Math.min(Number(totalCount) || 0, MAX_TOTAL));
const headers = {
'X-Naver-Client-Id': CLIENT_ID,
'X-Naver-Client-Secret': CLIENT_SECRET
};
const out = [];
let fetched = 0;
while (fetched < totalCount) {
const remaining = totalCount - fetched;
const display = Math.min(MAX_PER_REQUEST, remaining);
const start = fetched + 1; // 1-base, 최대 1000
const params = {
query: query,
display: display,
start: start,
sort: 'sim',
// 요구사항: 중고/렌탈/해외직구·구매대행 제외
exclude: 'used:rental:cbshop'
};
const url = API_URL + '?' + toQuery(params);
const res = UrlFetchApp.fetch(url, { method: 'get', headers: headers, muteHttpExceptions: true });
const code = res.getResponseCode();
if (code !== 200) {
throw new Error('네이버 API 오류: HTTP ' + code + ' / ' + res.getContentText());
}
const data = JSON.parse(res.getContentText());
const items = (data && data.items) || [];
if (!items.length) break;
items.forEach(it => {
out.push([
cleanText(it.title),
it.link || '',
it.image || '',
it.lprice || '',
it.hprice || '',
it.mallName || '',
it.brand || '',
it.maker || '',
it.category1 || '',
it.category2 || '',
it.category3 || '',
it.category4 || ''
]);
fetched++;
});
// API 예의상 살짝 휴식
Utilities.sleep(120);
}
// === 시트에 기록 ===
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheetName = 'naver_shopping_' + slugifyAscii(query) + '_' + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd_HHmmss');
const sh = ss.insertSheet(sheetName);
const header = [
'title','link','image','lprice','hprice','mallName',
'brand','maker','category1','category2','category3','category4'
];
sh.getRange(1,1,1,header.length).setValues([header]);
if (out.length) {
sh.getRange(2,1,out.length, header.length).setValues(out);
}
// 자동 너비
sh.autoResizeColumns(1, header.length);
return {
sheetUrl: ss.getUrl(),
sheetName: sheetName,
rowCount: out.length
};
}
/** 쿼리스트링 유틸 */
function toQuery(obj) {
return Object.keys(obj)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(String(obj[k])))
.join('&');
}