(() => {
const handleError = error => {
console.error(error);
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleError);
window.addEventListener('beforeunload', () => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleError);
});
})();
/* eslint-disable no-undef */
window['shopby'] = window.shopby || {
localStorage: null,
sessionStorage: null,
utils: null,
logined: null,
goHome: null,
config: {
skin: null,
pay: null,
},
platform: null,
api: null,
start: {
initiate: null,
entries: [],
},
cash: null,
regex: null,
message: null,
designPopup: null,
// page global object
home: null,
intro: {
noAccess: null,
memberOnly: null,
adultCertification: null,
},
product: {
view: null,
},
order: null,
cart: null,
member: {
login: null,
auth: null,
},
board: {
list: null,
reply: null,
},
my: null,
service: {
agreement: null,
},
helper: {
cart: null,
timer: null,
option: null,
login: null,
captcha: null,
},
support: null,
/**
* @param {'MAIN'|'PRODUCT'|'PRODUCT_SEARCH'|'PRODUCT_LIST'|'DISPLAY_SECTION'|'CART'|'ORDER'|'ORDER_COMPLETE'|'ORDER_DETAIL'|'MEMBER_JOIN_COMPLETE'} key
* @param {object} payload
*/
setGlobalVariableBy: () => {},
};
/**
* 전역 객체 생성.
*/
(() => {
/**
* @shopby.logined
* 로그인했는지 판별해 주는 함수
*/
shopby.logined = function () {
const token = shopby.cache.getAccessToken();
const isDormant = shopby.localStorage.getItem(shopby.cache.key.member.dormant) || false;
return token && token !== '' && !isDormant;
};
/**
* @shopby.goHome
* 지정한 홈화면으로 이동하는 함수.
*/
shopby.goHome = function () {
window.location.href = '/index.html';
};
/**
* @shopby.goLogin
* 로그인 화면으로 이동하는 함수
*
* @param nextUrl: string
*/
shopby.goLogin = (nextUrl = '') => {
const queryParams = nextUrl && typeof nextUrl === 'string' ? `?next-url=${encodeURIComponent(nextUrl)}` : '';
window.location.href = `/pages/login/login.html${queryParams}`;
};
shopby.goAdultCertification = (nextUrl = '', introType = 'ONLY_ADULT') => {
const nextQuery = nextUrl ? `&next-url=${nextUrl}` : '';
window.location.href = `/pages/intro/adult-certification.html?introType=${introType}${nextQuery}`;
};
/**
* 로그인 유저만 이동
*/
shopby.moveForLogined = e => {
if (shopby.logined()) {
window.location.href = e.currentTarget.pathname;
} else {
shopby.goLogin();
}
return false;
};
/**
* @shopby.confirmLogin
* 컨펌창 띄운 후 결과에 따라 로그인 화면으로 이동
*/
shopby.confirmLogin = () => {
shopby.confirm({ message: '로그인하셔야 본 서비스를 이용하실 수 있습니다.' }, ({ state }) => {
if (state !== 'ok') return;
shopby.goLogin();
});
};
/**
* @shopby.getScript
* 특정 스트립트를 바로 실행
* (ex) 동적으로 DOM 추가
*/
shopby.getScript = url => {
return $.getScript(url)
.done(() => {})
.fail(function () {
shopby.alert(`fail : check the url ( ${url} )`);
});
};
/**
* path 로 현재 윈도우 팝업인지 아닌지 체크
*/
const windowPopupPath = ['design-popup'];
shopby.isWindowPopup = () => {
return windowPopupPath.some(path => location.pathname.includes(path));
};
/**
* @shopby.localStorage
* 로컬스토리지를 관리하는 객체. 로컬스토리지에 직접적으로 접근하기 보다는 아래의 함수를 사용하세요.
*
* @shopby.localStorage.getItemWithExpire
* @shopby.localStorage.setItemWithExpire
*/
shopby.localStorage = {
expirationDateKey: '_expirationDate',
setItem(name, value) {
const data = JSON.stringify(value);
try {
window.localStorage.setItem(name, data);
return true;
} catch (err) {
console.error('setStorage: Error setting key [' + name + '] in localStorage: ' + JSON.stringify(err));
return false;
}
},
getItem(name) {
if (!name) return null;
try {
return JSON.parse(window.localStorage.getItem(name));
} catch (err) {
console.error('getStorage: Error reading key [' + name + '] from localStorage: ' + JSON.stringify(err));
return null;
}
},
removeItem(name) {
try {
window.localStorage.removeItem(name);
window.localStorage.removeItem(name + this.expirationDateKey);
return true;
} catch (err) {
console.error('removeStorage: Error removing key [' + name + '] from localStorage: ' + JSON.stringify(err));
return false;
}
},
setItemWithExpire(name, data, expire = 60 * 5 * 1000) {
const expireUnixTime = Date.now() + expire;
this.setItem(name, data);
this.setItem(name + this.expirationDateKey, expireUnixTime);
},
getItemWithExpire(name) {
if (!name) return null;
const expireUnixTime = this.getItem(name + this.expirationDateKey);
const expire = !!expireUnixTime && expireUnixTime < Date.now();
return expire ? null : this.getItem(name);
},
/**
* 중복을 허용하지 않고 최대 N개 까지 저장, 순서대로 저장함
* 순서대로 1,2,3입력하면 1 -> 2,1 -> 3,2,1
* 다시 2가 들어오면 2,3,1
*
* @author haekyu.cho
* @param key
* @param value
* @param max 최대 배열 크기
* @param expire 1일
*/
unshiftItemByOrder(key, value, expire = 60 * 60 * 24 * 1000, max = 10) {
let values = JSON.parse(this.getItemWithExpire(key) || '[]').filter(v => v !== value);
values.unshift(value);
if (values.length > max) {
values = values.slice(0, max);
}
this.setItemWithExpire(key, JSON.stringify(values), expire);
},
popItemByOrder(key, value, expire = 60 * 60 * 24 * 1000) {
const filtered = this.getItemsByParced(key).filter(v => v !== value);
this.setItemWithExpire(key, JSON.stringify(filtered), expire);
},
getItemsByParced(key) {
return JSON.parse(this.getItemWithExpire(key));
},
};
shopby.sessionStorage = {
expirationDateKey: '_expirationDate',
setItem(name, value) {
const data = JSON.stringify(value);
try {
window.sessionStorage.setItem(name, data);
return true;
} catch (err) {
console.error('setStorage: Error setting key [' + name + '] in sessionStorage: ' + JSON.stringify(err));
return false;
}
},
getItem(name) {
if (!name) return null;
try {
return JSON.parse(window.sessionStorage.getItem(name));
} catch (err) {
console.error('getStorage: Error reading key [' + name + '] from sessionStorage: ' + JSON.stringify(err));
return null;
}
},
removeItem(name) {
try {
window.sessionStorage.removeItem(name);
window.sessionStorage.removeItem(name + this.expirationDateKey);
return true;
} catch (err) {
console.error('removeStorage: Error removing key [' + name + '] from sessionStorage: ' + JSON.stringify(err));
return false;
}
},
setItemWithExpire(name, data, expire = 60 * 5 * 1000) {
const expireUnixTime = Date.now() + expire;
this.setItem(name, data);
this.setItem(name + this.expirationDateKey, expireUnixTime);
},
getItemWithExpire(name) {
if (!name) return null;
const expireUnixTime = this.getItem(name + this.expirationDateKey);
const expire = !!expireUnixTime && expireUnixTime < Date.now();
return expire ? null : this.getItem(name);
},
};
/**
* @shopby.regex
* 전역 정규식 오브젝트.
*/
shopby.regex = {
number: /[0-9]/g,
notNumber: /[^0-9]/g,
negativeNumber: /[0-9-]+/g,
decimalNumber: /[0-9.]+/g,
eng: /[a-zA-Z]/g,
ko: /[ㄱ-힣]/g,
koEng: /[a-zA-Z0-9ㄱ-힣]/g,
currency: /[0-9,]/g,
at: /@/g,
noSpace: /\s/g, // 공백만 불가
space: /(?:\r\n|\r|\n)/g, // 줄바꿈
noCommonSpecial: /['"<>₩\\`‘”]/gi, // 공통 제한 문자 불가
noPartSpecial: /['"‘”<>\\`(),:;@[\]\s]/g, // 일부 특수문자(‘”<>\`(),:;@[])와 공백 불가
noSpecialSpace: /[^a-zA-Z0-9ㄱ-힣\s]/g, // 한글, 영문, 숫자, 공백만 가능
/* eslint-disable */
userid: /[^a-zA-Z0-9@\._\-]/g,
passwordSpecial: /[!@#$%^&+=\-_.()]/g, // 특수문자 : ! @ # $ % ^ & + = - _ . ( ) 만 사용 가능
emailId: /^[_A-Za-z0-9-\\+]+(.[_A-Za-z0-9-]+)$/,
emailDomain: /^[A-Za-z0-9-]+(.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$/,
email: /[_A-Za-z0-9-\\+]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9]+)*(.[A-Za-z]{2,})$/,
mobileNo: /(\d{11,12})/g,
birthday: /^(19[0-9][0-9]|20\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/,
customsId: /^[p|P][0-9]{12}$/, // 개인통관고유번호 (숫자, 영어 p가 아닌 영어, p + 영어, 한글, p+특수문자 제외)
engNumber: /[^0-9a-zA-Z]/g, //영문, 숫자만 가능
imageExtension: /.(bmp|png|jpg|jpeg|gif)$/i,
bankDepositorName: /[^a-zA-Zㄱ-힣!@#$%^&+=\-_.()]/g, //한글,영문대소문자,특수문자 : ! @ # $ % ^ & + = - _ . ( ) 만 사용 가능
/* eslint-enable */
};
/**
* @shopby.message
* 전역 메시지 모음
*/
shopby.message = {
invalidId: '영문, 숫자, 특수문자(-),(_),(.),(@)만 입력가능 합니다. (@는 1개만 입력 가능)',
lessLengthId: '아이디는 최소 5자 이상 입력해주세요.',
existId: '이미 사용중인 아이디입니다. 다른 아이디를 입력하여 주세요.',
withDrawn: '회원 탈퇴한 아이디 입니다. 탈퇴 후 동일한 아이디로 5일간 재가입 불가합니다.',
kcpWithdrawn: '탈퇴한 회원입니다. 탈퇴 후 5일간 재가입 불가합니다.',
kcpExistMember: '이미 가입된 회원입니다.',
invalidPassword: '영문, 숫자, 특수문자를 3종류 모두 조합하여 8~20자로 입력해주세요.',
lessInvalidPassword: '영문, 숫자, 특수문자를 2종류 이상 조합하여 10~20자로 입력해주세요.',
invalidPasswordSpecial: '특수문자는 !@#$%^&+=-_.()만 사용 가능합니다.',
notEqualPassword: '비밀번호와 비밀번호확인 값이 일치하지 않습니다.',
notEqualNewPassword: '새 비밀번호와 새 비밀번호확인 값이 일치하지 않습니다.',
noInputEmail: '이메일을 입력해주세요.',
invalidEmail: '입력된 이메일은 잘못된 형식입니다.',
existEmail: '이메일 주소가 이미 사용중입니다.',
existNickname: '닉네임이 이미 사용중입니다.',
invalidKorEngNum: '한글, 영문, 숫자만 입력 가능합니다.',
invalidKorEng: '한글, 영문만 입력가능합니다.',
invalidMobileNo: '11~12자 이내로 입력해주세요.',
invalidAge: '만 14세 미만은 쇼핑몰 이용이 불가합니다.',
invalidBirthday: '생년월일을 확인해주세요.',
requiredBirthday: '생년월일을 선택해주세요.',
successId: '사용 가능한 아이디입니다.',
successEmail: '사용 가능한 이메일입니다.',
successNickname: '사용 가능한 닉네임입니다.',
};
/**
* @shopby.utils
* 전역 유틸리티 함수.
*/
shopby.utils = {
getPlatform() {
return /(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent) ? 'MOBILE_WEB' : 'PC';
},
getUrlParam(name, defaultValue = '') {
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has(name) && Boolean(searchParams.get(name))) {
return decodeURIComponent(searchParams.get(name));
}
return defaultValue;
},
getUrlParams() {
const urlSearchParams = new URLSearchParams(window.location.search);
return Object.fromEntries(urlSearchParams.entries());
},
/**
* @pushState 브라우저 리프래쉬 없이 queryString 추가
*
* @author Jongkeun Kim
* @param params : object | null
* @param title : string
*/
pushState(params = null, title = document.title) {
const path = window.location.pathname;
const queryString = new URLSearchParams({ ...shopby.utils.getUrlParams(), ...params });
const url = params ? `${path}?${queryString}` : path;
window.history.pushState(null, title, url);
},
/**
* @replaceState 브라우저 리프래쉬 없이 queryString 추가. 히스토리 저장 X
*
* @author Jongkeun Kim
* @param params : object | null
* @param title : string
*/
replaceState(params = null, title = document.title) {
const path = window.location.pathname;
const queryString = new URLSearchParams({ ...shopby.utils.getUrlParams(), ...params });
const url = params ? `${path}?${queryString}` : path;
window.history.replaceState(null, title, url);
},
/**
* 쿼리스트링 변경
*
* @param params - {name: value}
*/
sendQueryString(params) {
const paramEntries = new URLSearchParams(location.search).entries();
const existingParams = {};
for (const [key, value] of paramEntries) {
existingParams[key] = value;
}
const newParams = Object.assign(existingParams, params);
location.href = `${window.location.pathname}?${$.param(newParams)}`;
},
/**
*
ary가 배열이고 배열 사이즈가 0보다 클 경우
*
* @param ary
* @returns {boolean}
*/
isArrayNotEmpty(ary) {
return Array.isArray(ary) && ary.length > 0;
},
isArrayEmpty(ary) {
return Array.isArray(ary) && ary.length === 0;
},
/**
* locales는 아래와 같다
* 'en-US', // United States
* 'ko-KR', // korea
* undefined, // 브라우저에게 locaels를 맡긴다.
*
* @param num
* @param signDisplay
* @returns {string}
*/
toCurrencyString(num = 0, signDisplay = 'auto') {
// return String(num).replace(/(.)(?=(\d{3})+$)/g, '$1,'); 정규식을 이용하는 방법도 있다.
return num.toLocaleString(undefined, { minimumFractionDigits: 0, signDisplay });
},
substrWithPostFix(str, length = 73, postFix = '...') {
const postFixStr = str.length > length ? postFix : '';
return str.substr(0, length).concat(postFixStr);
},
changeBreadcrumb(lastLocation) {
$('.location_cont').html(` HOME > ${lastLocation}`);
},
getDisplayProductPrice({ salePrice, immediateDiscountAmt, additionDiscountAmt }) {
return (salePrice || 0) - (immediateDiscountAmt || 0) - (additionDiscountAmt || 0);
},
getTotal(objectArray, keyName) {
return objectArray.reduce((sum, object) => (sum += object[keyName]), 0);
},
// 상품합계금액
// getDiscountAmt 의존 존재
getProductTotalAmt({ salePrice, addPrice, immediateDiscountAmt, additionalDiscountAmt }, orderCnt = 1) {
const discountAmt = this.getDiscountAmt({ immediateDiscountAmt, additionalDiscountAmt });
return orderCnt * (salePrice - discountAmt + addPrice);
},
// 할인 금액
getDiscountAmt({ immediateDiscountAmt, additionalDiscountAmt }, orderCnt = 1) {
return (immediateDiscountAmt + additionalDiscountAmt) * orderCnt;
},
//옵션 정보 생성
createOptionText(orderOption) {
if (!orderOption) return '';
const seperator = '|';
const values = orderOption.optionValue.split(seperator);
const optionText = orderOption.optionName
.split(seperator)
.map((name, index) => `${name} : ${values[index]}`)
.join(` ${seperator} `);
const inputText = orderOption.inputs
? orderOption.inputs
.filter(input => !!input.inputValue)
.map(input => `- ${input.inputLabel} : ${input.inputValue}`)
.join('
')
: '';
return optionText.concat('
').concat(inputText ? inputText : '');
},
isPreSalePeriod(reservationData) {
if (!reservationData) return false;
const { reservationStartYmdt, reservationEndYmdt } = reservationData;
const today = dayjs();
const saleStart = today.diff(dayjs(reservationStartYmdt)) >= 0;
const saleFinish = today.diff(dayjs(reservationEndYmdt)) <= 0;
return saleStart && saleFinish;
},
/**
* @throttle
*
* @author Jongkeun Kim
* @callback function : 콜백
* @limit number : ms second
*/
throttle(callback, limit) {
let waiting = false;
return () => {
if (!waiting) {
callback.apply(this, arguments);
waiting = true;
setTimeout(() => {
waiting = false;
}, limit);
}
};
},
/**
* @throttleUsingRaf requestAnimationFrame을 사용한 throttle
*
* @author eunbi kim
* @callback function : 콜백
*/
throttleUsingRaf(callback) {
let timeout = null;
return function () {
if (timeout) {
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(() => {
callback();
});
};
},
/**
* @hideAllContents 현재 페이지를 아예 보이지 않게 할 경우 사용
*
* @author sohyun choi
*/
hideAllContents() {
$('#contents,#header,#aside,#footer').hide();
},
/**
* 텍스트 말줄임
*
* @author YoungGeun Kwon
* @param text
* @param count
* @returns {string|*}
*/
truncate(text, count) {
if (text.includes('\n') && text.indexOf('\n') < count) {
return text.substr(0, text.indexOf('\n')) + '...';
}
if (text.length >= count) {
return text.substr(0, count) + '...';
}
return text;
},
removeComma(currency) {
return currency.replace(shopby.regex['notNumber'], '');
},
/*
* originObject 중에서 keys 에 해당되는 것만 추출하여 반환
*
* @author Bomee Yoon
* @param originObject
* @param keys
* @return pickedObject
* */
pickObjectByKeys(originObject, keys) {
return keys.reduce((pickedObject, key) => {
if (originObject && originObject[key]) {
const originValue = originObject[key];
if (Object.prototype.toString.call(originValue).includes('object Object')) {
pickedObject[key] = { ...originValue };
} else {
pickedObject[key] = originValue;
}
}
return pickedObject;
}, {});
},
/* eslint-disable */
// prettier-ignore
isEqual(t, e) { var r = 200, n = "__lodash_hash_undefined__", o = 1, i = 2, a = 9007199254740991, u = "[object Arguments]", c = "[object Array]", s = "[object AsyncFunction]", f = "[object Boolean]", l = "[object Date]", _ = "[object Error]", h = "[object Function]", p = "[object GeneratorFunction]", v = "[object Map]", y = "[object Number]", b = "[object Null]", d = "[object Object]", g = "[object Proxy]", j = "[object RegExp]", w = "[object Set]", m = "[object String]", z = "[object Symbol]", A = "[object Undefined]", O = "[object ArrayBuffer]", k = "[object DataView]", S = /^\[object .+?Constructor\]$/, x = /^(?:0|[1-9]\d*)$/, E = {}; E["[object Float32Array]"] = E["[object Float64Array]"] = E["[object Int8Array]"] = E["[object Int16Array]"] = E["[object Int32Array]"] = E["[object Uint8Array]"] = E["[object Uint8ClampedArray]"] = E["[object Uint16Array]"] = E["[object Uint32Array]"] = !0, E[u] = E[c] = E[O] = E[f] = E[k] = E[l] = E[_] = E[h] = E[v] = E[y] = E[d] = E[j] = E[w] = E[m] = E["[object WeakMap]"] = !1; var P = "object" == typeof global && global && global.Object === Object && global, F = "object" == typeof self && self && self.Object === Object && self, $ = P || F || Function("return this")(), M = "object" == typeof exports && exports && !exports.nodeType && exports, U = M && "object" == typeof module && module && !module.nodeType && module, B = U && U.exports === M, I = B && P.process, L = function () { try { return I && I.binding && I.binding("util") } catch (t) { } }(), T = L && L.isTypedArray; function W(t, e) { for (var r = -1, n = null == t ? 0 : t.length; ++r < n;)if (e(t[r], r, t)) return !0; return !1 } function D(t) { var e = -1, r = Array(t.size); return t.forEach(function (t, n) { r[++e] = [n, t] }), r } function R(t) { var e = -1, r = Array(t.size); return t.forEach(function (t) { r[++e] = t }), r } var C, N, V, q = Array.prototype, G = Function.prototype, H = Object.prototype, J = $["__core-js_shared__"], K = G.toString, Q = H.hasOwnProperty, X = (C = /[^.]+$/.exec(J && J.keys && J.keys.IE_PROTO || "")) ? "Symbol(src)_1." + C : "", Y = H.toString, Z = RegExp("^" + K.call(Q).replace(/[\\^$.*+?()[\]{}|]/g, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"), tt = B ? $.Buffer : void 0, et = $.Symbol, rt = $.Uint8Array, nt = H.propertyIsEnumerable, ot = q.splice, it = et ? et.toStringTag : void 0, at = Object.getOwnPropertySymbols, ut = tt ? tt.isBuffer : void 0, ct = (N = Object.keys, V = Object, function (t) { return N(V(t)) }), st = It($, "DataView"), ft = It($, "Map"), lt = It($, "Promise"), _t = It($, "Set"), ht = It($, "WeakMap"), pt = It(Object, "create"), vt = Dt(st), yt = Dt(ft), bt = Dt(lt), dt = Dt(_t), gt = Dt(ht), jt = et ? et.prototype : void 0, wt = jt ? jt.valueOf : void 0; function mt(t) { var e = -1, r = null == t ? 0 : t.length; for (this.clear(); ++e < r;) { var n = t[e]; this.set(n[0], n[1]) } } function zt(t) { var e = -1, r = null == t ? 0 : t.length; for (this.clear(); ++e < r;) { var n = t[e]; this.set(n[0], n[1]) } } function At(t) { var e = -1, r = null == t ? 0 : t.length; for (this.clear(); ++e < r;) { var n = t[e]; this.set(n[0], n[1]) } } function Ot(t) { var e = -1, r = null == t ? 0 : t.length; for (this.__data__ = new At; ++e < r;)this.add(t[e]) } function kt(t) { var e = this.__data__ = new zt(t); this.size = e.size } function St(t, e) { var r = Nt(t), n = !r && Ct(t), o = !r && !n && Vt(t), i = !r && !n && !o && Kt(t), a = r || n || o || i, u = a ? function (t, e) { for (var r = -1, n = Array(t); ++r < t;)n[r] = e(r); return n }(t.length, String) : [], c = u.length; for (var s in t) !e && !Q.call(t, s) || a && ("length" == s || o && ("offset" == s || "parent" == s) || i && ("buffer" == s || "byteLength" == s || "byteOffset" == s) || Wt(s, c)) || u.push(s); return u } function xt(t, e) { for (var r = t.length; r--;)if (Rt(t[r][0], e)) return r; return -1 } function Et(t) { return null == t ? void 0 === t ? A : b : it && it in Object(t) ? function (t) { var e = Q.call(t, it), r = t[it]; try { t[it] = void 0; var n = !0 } catch (t) { } var o = Y.call(t); n && (e ? t[it] = r : delete t[it]); return o }(t) : function (t) { return Y.call(t) }(t) } function Pt(t) { return Jt(t) && Et(t) == u } function Ft(t) { return !(!Ht(t) || X && X in t) && (qt(t) ? Z : S).test(Dt(t)) } function $t(t) { if (r = (e = t) && e.constructor, n = "function" == typeof r && r.prototype || H, e !== n) return ct(t); var e, r, n, o = []; for (var i in Object(t)) Q.call(t, i) && "constructor" != i && o.push(i); return o } function Mt(t, e, r, n, a, u) { var c = r & o, s = t.length, f = e.length; if (s != f && !(c && f > s)) return !1; var l = u.get(t); if (l && u.get(e)) return l == e; var _ = -1, h = !0, p = r & i ? new Ot : void 0; for (u.set(t, e), u.set(e, t); ++_ < s;) { var v = t[_], y = e[_]; if (n) var b = c ? n(y, v, _, e, t, u) : n(v, y, _, t, e, u); if (void 0 !== b) { if (b) continue; h = !1; break } if (p) { if (!W(e, function (t, e) { if (o = e, !p.has(o) && (v === t || a(v, t, r, n, u))) return p.push(e); var o })) { h = !1; break } } else if (v !== y && !a(v, y, r, n, u)) { h = !1; break } } return u.delete(t), u.delete(e), h } function Ut(t) { return function (t, e, r) { var n = e(t); return Nt(t) ? n : function (t, e) { for (var r = -1, n = e.length, o = t.length; ++r < n;)t[o + r] = e[r]; return t }(n, r(t)) }(t, Qt, Lt) } function Bt(t, e) { var r, n, o = t.__data__; return ("string" == (n = typeof (r = e)) || "number" == n || "symbol" == n || "boolean" == n ? "__proto__" !== r : null === r) ? o["string" == typeof e ? "string" : "hash"] : o.map } function It(t, e) { var r = function (t, e) { return null == t ? void 0 : t[e] }(t, e); return Ft(r) ? r : void 0 } mt.prototype.clear = function () { this.__data__ = pt ? pt(null) : {}, this.size = 0 }, mt.prototype.delete = function (t) { var e = this.has(t) && delete this.__data__[t]; return this.size -= e ? 1 : 0, e }, mt.prototype.get = function (t) { var e = this.__data__; if (pt) { var r = e[t]; return r === n ? void 0 : r } return Q.call(e, t) ? e[t] : void 0 }, mt.prototype.has = function (t) { var e = this.__data__; return pt ? void 0 !== e[t] : Q.call(e, t) }, mt.prototype.set = function (t, e) { var r = this.__data__; return this.size += this.has(t) ? 0 : 1, r[t] = pt && void 0 === e ? n : e, this }, zt.prototype.clear = function () { this.__data__ = [], this.size = 0 }, zt.prototype.delete = function (t) { var e = this.__data__, r = xt(e, t); return !(r < 0 || (r == e.length - 1 ? e.pop() : ot.call(e, r, 1), --this.size, 0)) }, zt.prototype.get = function (t) { var e = this.__data__, r = xt(e, t); return r < 0 ? void 0 : e[r][1] }, zt.prototype.has = function (t) { return xt(this.__data__, t) > -1 }, zt.prototype.set = function (t, e) { var r = this.__data__, n = xt(r, t); return n < 0 ? (++this.size, r.push([t, e])) : r[n][1] = e, this }, At.prototype.clear = function () { this.size = 0, this.__data__ = { hash: new mt, map: new (ft || zt), string: new mt } }, At.prototype.delete = function (t) { var e = Bt(this, t).delete(t); return this.size -= e ? 1 : 0, e }, At.prototype.get = function (t) { return Bt(this, t).get(t) }, At.prototype.has = function (t) { return Bt(this, t).has(t) }, At.prototype.set = function (t, e) { var r = Bt(this, t), n = r.size; return r.set(t, e), this.size += r.size == n ? 0 : 1, this }, Ot.prototype.add = Ot.prototype.push = function (t) { return this.__data__.set(t, n), this }, Ot.prototype.has = function (t) { return this.__data__.has(t) }, kt.prototype.clear = function () { this.__data__ = new zt, this.size = 0 }, kt.prototype.delete = function (t) { var e = this.__data__, r = e.delete(t); return this.size = e.size, r }, kt.prototype.get = function (t) { return this.__data__.get(t) }, kt.prototype.has = function (t) { return this.__data__.has(t) }, kt.prototype.set = function (t, e) { var n = this.__data__; if (n instanceof zt) { var o = n.__data__; if (!ft || o.length < r - 1) return o.push([t, e]), this.size = ++n.size, this; n = this.__data__ = new At(o) } return n.set(t, e), this.size = n.size, this }; var Lt = at ? function (t) { return null == t ? [] : (t = Object(t), function (t, e) { for (var r = -1, n = null == t ? 0 : t.length, o = 0, i = []; ++r < n;) { var a = t[r]; e(a, r, t) && (i[o++] = a) } return i }(at(t), function (e) { return nt.call(t, e) })) } : function () { return [] }, Tt = Et; function Wt(t, e) { return !!(e = null == e ? a : e) && ("number" == typeof t || x.test(t)) && t > -1 && t % 1 == 0 && t < e } function Dt(t) { if (null != t) { try { return K.call(t) } catch (t) { } try { return t + "" } catch (t) { } } return "" } function Rt(t, e) { return t === e || t != t && e != e } (st && Tt(new st(new ArrayBuffer(1))) != k || ft && Tt(new ft) != v || lt && "[object Promise]" != Tt(lt.resolve()) || _t && Tt(new _t) != w || ht && "[object WeakMap]" != Tt(new ht)) && (Tt = function (t) { var e = Et(t), r = e == d ? t.constructor : void 0, n = r ? Dt(r) : ""; if (n) switch (n) { case vt: return k; case yt: return v; case bt: return "[object Promise]"; case dt: return w; case gt: return "[object WeakMap]" }return e }); var Ct = Pt(function () { return arguments }()) ? Pt : function (t) { return Jt(t) && Q.call(t, "callee") && !nt.call(t, "callee") }, Nt = Array.isArray; var Vt = ut || function () { return !1 }; function qt(t) { if (!Ht(t)) return !1; var e = Et(t); return e == h || e == p || e == s || e == g } function Gt(t) { return "number" == typeof t && t > -1 && t % 1 == 0 && t <= a } function Ht(t) { var e = typeof t; return null != t && ("object" == e || "function" == e) } function Jt(t) { return null != t && "object" == typeof t } var Kt = T ? function (t) { return function (e) { return t(e) } }(T) : function (t) { return Jt(t) && Gt(t.length) && !!E[Et(t)] }; function Qt(t) { return null != (e = t) && Gt(e.length) && !qt(e) ? St(t) : $t(t); } return function t(e, r, n, a, s) { return e === r || (null == e || null == r || !Jt(e) && !Jt(r) ? e != e && r != r : function (t, e, r, n, a, s) { var h = Nt(t), p = Nt(e), b = h ? c : Tt(t), g = p ? c : Tt(e), A = (b = b == u ? d : b) == d, S = (g = g == u ? d : g) == d, x = b == g; if (x && Vt(t)) { if (!Vt(e)) return !1; h = !0, A = !1 } if (x && !A) return s || (s = new kt), h || Kt(t) ? Mt(t, e, r, n, a, s) : function (t, e, r, n, a, u, c) { switch (r) { case k: if (t.byteLength != e.byteLength || t.byteOffset != e.byteOffset) return !1; t = t.buffer, e = e.buffer; case O: return !(t.byteLength != e.byteLength || !u(new rt(t), new rt(e))); case f: case l: case y: return Rt(+t, +e); case _: return t.name == e.name && t.message == e.message; case j: case m: return t == e + ""; case v: var s = D; case w: var h = n & o; if (s || (s = R), t.size != e.size && !h) return !1; var p = c.get(t); if (p) return p == e; n |= i, c.set(t, e); var b = Mt(s(t), s(e), n, a, u, c); return c.delete(t), b; case z: if (wt) return wt.call(t) == wt.call(e) }return !1 }(t, e, b, r, n, a, s); if (!(r & o)) { var E = A && Q.call(t, "__wrapped__"), P = S && Q.call(e, "__wrapped__"); if (E || P) { var F = E ? t.value() : t, $ = P ? e.value() : e; return s || (s = new kt), a(F, $, r, n, s) } } return !!x && (s || (s = new kt), function (t, e, r, n, i, a) { var u = r & o, c = Ut(t), s = c.length, f = Ut(e).length; if (s != f && !u) return !1; for (var l = s; l--;) { var _ = c[l]; if (!(u ? _ in e : Q.call(e, _))) return !1 } var h = a.get(t); if (h && a.get(e)) return h == e; var p = !0; a.set(t, e), a.set(e, t); for (var v = u; ++l < s;) { _ = c[l]; var y = t[_], b = e[_]; if (n) var d = u ? n(b, y, _, e, t, a) : n(y, b, _, t, e, a); if (!(void 0 === d ? y === b || i(y, b, r, n, a) : d)) { p = !1; break } v || (v = "constructor" == _) } if (p && !v) { var g = t.constructor, j = e.constructor; g != j && "constructor" in t && "constructor" in e && !("function" == typeof g && g instanceof g && "function" == typeof j && j instanceof j) && (p = !1) } return a.delete(t), a.delete(e), p }(t, e, r, n, a, s)) }(e, r, n, a, t, s)) }(t, e) },
/* eslint-enable */
/*
* Promise.allSettled polyfill
* */
allSettled(promises) {
return Promise.all(
promises.map(p =>
Promise.resolve(p).then(
value => ({
status: 'fulfilled',
value,
}),
reason => ({
status: 'rejected',
reason,
}),
),
),
);
},
convertMinutesToMilliseconds(time) {
return 1000 * 60 * time;
},
convertDaysToMilliseconds(days) {
return 24 * 3600 * 1000 * days;
},
getPrefixPathname(regExp) {
const { pathname } = window.location;
const prefixUrl = pathname.match(regExp)[0];
return prefixUrl.substring(0, prefixUrl.length - 1);
},
findDomainBy(platform) {
const urls = shopby.cache.getMall().mall.url;
const domain = urls[platform];
if (!domain) return null;
const domainInstance = new URL(domain);
return `${location.origin}${domainInstance.pathname}`.replace(/\/$/, '');
},
getRedirectUrl(nextUrl = '') {
const isMobile = shopby.utils.getPlatform() === 'MOBILE_WEB';
const platform = isMobile ? 'mobile' : 'pc';
const domain = shopby.utils.findDomainBy(platform);
if (domain) {
return encodeURI(`${domain}${nextUrl}`);
}
const { origin } = window.location;
return encodeURI(`${origin}${nextUrl}`);
},
getPathNameWithoutPlatform(pathname = '') {
const pathnameWithoutPlatformType = pathname.replace(/\/m\/?(?![a-z])/, '');
return pathnameWithoutPlatformType ? pathnameWithoutPlatformType : '/';
},
};
/**
* @shopby.config
* 샵 설정 절보.
*
* @shopby.config.apiUrl : shop API 주소
* @shopby.config.storageApiUrl : 첨부파일 Storage 주소
* @shopby.config.skin : 스킨
* @shopby.config.pay : 결제 수단
*/
shopby.config = {
apiUrl: null,
storageApiUrl: null,
skin: {
profile: null,
clientId: null,
osType: 'WEB',
platform: 'PC',
mallName: null,
sections: {
pc: ['SCPC0001', 'SCPC0002', 'SCPC0003', 'SCPC0004', 'SCPC0005'],
mobile: ['SCMO0001', 'SCMO0002', 'SCMO0003', 'SCMO0004', 'SCMO0005'],
},
// skinCode: null,
bannersGroup: null,
bannerGroupCodes: [],
supportOauth: {
payco: true,
naver: true,
kakao: true,
facebook: true,
},
pageWidth: 'normal',
instagramAccessToken: '7309523338.e5fa3ea.a5335074c1424e9a974cd54cc9d1b900',
mobileLogoUrl: '',
privacyConsignmentUse: true,
privacyThirdPartyUse: true,
getBannerGroupCode: groupType => {
const hasBannerGroups = Array.isArray(shopby.config.skin.bannerGroups);
const bannerGroup = hasBannerGroups
? shopby.config.skin.bannerGroups.find(banner => banner.groupType === groupType)
: null;
return bannerGroup ? bannerGroup.groupCode : '';
},
},
member: {
joinConfig: {
address: 'USED',
birthday: 'USED',
email: 'REQUIRED',
memberId: 'REQUIRED',
memberName: 'REQUIRED',
mobileNo: 'REQUIRED',
nationality: 'NOT_USED',
nickname: 'USED',
password: 'REQUIRED',
phoneNo: 'USED',
sex: 'USED',
},
},
naverPay: {
button: {
pc: 'https://test-pay.naver.com/customer/js/naverPayButton.js',
mobile: 'https://pay.naver.com/customer/js/mobile/naverPayButton.js"',
},
},
pay: {
js: 'ncp_pay_alpha.js',
plugins: [
'https://testpay.kcp.co.kr/plugin/payplus_web.jsp',
'https://xpay-sandbox.tosspayments.com/xpay/js/xpay_crossplatform.js',
],
},
};
/**
* @shopby.api
* 전역 Shop API..
*/
(() => {
const api = {
initiate() {
this.setPlatform();
this.setApi();
},
setPlatform() {
const platformClassification = {
PC: 'pc',
MOBILE_WEB: 'mobile',
};
const deployPlatform = shopby.config.skin.platform;
shopby.platform = platformClassification[deployPlatform];
},
// TODO. 우선 startsWith 로 요청중 매치되는 url 이 있는지 찾아서 401 시 매칭되는 credentialLevel(require, optional) 에 따라 다른 통합 처리를 함
credentialLevelUrl: {
guest: ['/guest'],
required: ['/profile', '/coupons', '/cart', '/kcp', '/previous-orders', '/my'],
optional: ['/boards', '/products', '/order-sheets', '/cart/count', '/display', '/inquiries'],
},
/**
* @reason : 'TOKEN_EMPTY'
*/
// eslint-disable-next-line no-unused-vars
requestFailure(reason = 'UNKNOWN') {
const FAIL_REASON = {
TOKEN_EMPTY: '로그인이 필요한 서비스입니다.',
UNKNOWN: '알수없는 사유',
};
if (reason === 'TOKEN_EMPTY') {
shopby.alert(FAIL_REASON[reason], shopby.goLogin);
}
return new Promise((_, reject) => reject(FAIL_REASON[reason]));
},
/**
* @reason : 'TOKEN_EXPIRED'
*/
// eslint-disable-next-line no-unused-vars
responseInterceptor(reason = 'UNKHOWN') {
const FAIL_REASON = {
TOKEN_EXPIRED: '인증 정보가 만료되었습니다.',
UNKNOWN: '알수없는 사유',
};
if (reason === 'TOKEN_EXPIRED') {
shopby.alert(FAIL_REASON[reason], shopby.goLogin);
}
return new Promise((_, reject) => reject(FAIL_REASON[reason]));
},
async handleResponse(response, credentialLevel, originalRequestUrl, originalRequest) {
const { status, url } = response;
const _url = url || '';
const data = _url.includes('/kcp') ? await response.text() : await response.json().catch(() => null);
const noAlertCode = ['M0010', 'ODSH0010', 'NCPE0002', 'M0020'];
if (status === 503) {
location.href = '/pages/error_503.html';
return;
}
if (status === 401) {
shopby.sessionStorage.removeItem(shopby.cache.dataKey.profile);
}
// 토큰이 있는데 유효하지 않을 경우 통합처리
const unAuthorizedErrorCode = 'NCPE0003';
if (status === 401 && data.code === unAuthorizedErrorCode) {
shopby.cache.removeAccessToken();
shopby.goHome();
return;
}
// 토큰이 있는데 유효하지 않을 경우 통합처리
if (status === 401 && data.code === 'M0013') {
shopby.cache.removeAccessToken();
shopby.goHome();
return;
}
if (_url.includes('/malls') && status === 400 && data.code === 'M0009') {
location.replace('/pages/expired-mall.html');
return;
}
if (_url.includes('/authentications') && status === 401) {
throw data;
}
if (data && noAlertCode.includes(data.code)) {
throw data;
}
if (status === 400 || status === 404 || status === 500 || status === 503 || status === 401) {
/**
* @error : { status, message }
*/
const ERROR_CODE = {
CART: {
ADULT_PRODUCT: 'PPVE0003',
NO_EXHIBITION: 'PPVE0019',
STOP_SALE: 'PPVE0013',
NO_SALE: 'PPVE0016',
WAIT_SALE: 'PPVE0014',
NO_OPTION: 'PPVE0001',
SOLDOUT_OPTION: 'PPVE0011',
},
MY_ORDER: {
NO_EXIST_ORDER: 'OD0005',
NO_MY_ORDER: 'O0016',
NO_GUEST_MY_ORDER: 'NCPE0010',
UNAUTHENTICATED: 'O3336',
SMS_UNAUTHENTICATED: 'O3338',
COUPON: 'C0021',
},
BANNER: {
NO_EXIST_BANNER: 'SBEC0001',
},
};
const parseJsonSafely = data => {
try {
return typeof data === 'string' ? JSON.parse(data) : { ...data }; // 서버에서 주는 error 객체
} catch (e) {
console.error(e);
return {};
}
};
const error = parseJsonSafely(data);
const hasMessage = error.result && error.result.message;
console.error(error);
if (!this.ignorableError(error, ERROR_CODE)) {
this.customErrorMessage(error, ERROR_CODE);
shopby.alert(error.message || hasMessage || response.statusText, () => {
this.afterErrorAction(error, ERROR_CODE);
});
throw error;
}
}
// @fixme: /profile 내부에 optional token 인 경우가 있어 임시로 주석처리하였습니다.
// if (credentialLevel === 'required' && status === 401) return this.responseInterceptor('TOKEN_EXPIRED');
if (credentialLevel === 'optional' && status === 401) {
// shopby.cache.removeAccessToken();
// 필수인증여부 : 옵셔널 이고 401 리턴(토큰만료) 시 토큰 제외하고 재요청
delete originalRequest.headers.accessToken;
const response = await fetch(originalRequestUrl, originalRequest);
return await this.handleResponse(response);
}
return {
status,
data,
};
},
setApi() {
shopby.api = new ShopSDK(async option => {
const { method, requestBody, headers } = option;
const credentialLevels =
Object.entries(this.credentialLevelUrl).find((
[_, value], // eslint-disable-line no-unused-vars
) => value.some(url => option.url.startsWith(url))) || [];
const credentialLevel = credentialLevels.length > 0 ? credentialLevels[0] : null;
const isFormData = option.requestBody instanceof FormData;
const apiUrl = isFormData ? shopby.config.storageApiUrl : shopby.config.apiUrl;
const queryParams = new URLSearchParams(option.queryString).toString();
const queryString = !queryParams ? '' : `?${queryParams}`;
const requestUrl = apiUrl + option.url + queryString;
const version = headers ? headers.Version : '1.0';
const requestInit = {
method,
headers: {
//'Content-Type': 'application/json; charset=utf-8',
platform: 'PC',
clientId: shopby.config.skin.clientId,
Version: version,
},
body: isFormData ? requestBody : JSON.stringify(requestBody),
};
if (!isFormData) {
requestInit.headers['Content-Type'] = 'application/json; charset=utf-8';
}
if (!credentialLevel) {
const response = await fetch(requestUrl, requestInit);
return await this.handleResponse(response);
}
const accessToken = shopby.cache.getAccessToken();
const guestToken = shopby.cache.getGuestToken();
// @fixme: /profile 내부에 optional token인 경우가 있어 임시로 주석처리하였습니다.
// if (credentialLevel === 'required' && !accessToken) return this.requestFailure('TOKEN_EMPTY');
// @fixme: /guest 쪽 게스트 토큰 예외처리 수정하기 cc. jongkeun
if (credentialLevel === 'guest') {
if (guestToken) Object.assign(requestInit.headers, { guestToken });
} else {
if (accessToken) Object.assign(requestInit.headers, { accessToken });
if (guestToken) Object.assign(requestInit.headers, { guestToken });
}
const response = await fetch(requestUrl, requestInit);
return await this.handleResponse(response, credentialLevel, requestUrl, requestInit);
});
},
customErrorMessage(error, ERROR_CODE) {
if (error.code === ERROR_CODE.CART.ADULT_PRODUCT) {
shopby.confirm(
{ message: '해당 상품은 성인전용 상품입니다.성인인증 후 상품을 구매하시겠습니까?' },
({ state }) => (state === 'ok' ? shopby.goAdultCertification() : shopby.goHome()),
);
throw error;
}
if (
error.code === ERROR_CODE.MY_ORDER.UNAUTHENTICATED ||
error.code === ERROR_CODE.MY_ORDER.SMS_UNAUTHENTICATED
) {
shopby.confirm({ message: '인증 후 구매가 가능합니다. 본인인증 페이지로 이동하시겠습니까?' }, ({ state }) => {
if (state === 'ok') window.location.href = `/pages/my/modify-member.html`;
});
throw error;
}
if (error.code === ERROR_CODE.MY_ORDER.COUPON) {
shopby.alert('프로모션 코드를 확인해주세요.');
throw error;
}
if (error.code === ERROR_CODE.CART.SOLDOUT_OPTION) {
shopby.alert('재고가 부족합니다. 수량을 조정해주세요.');
throw error;
}
},
ignorableError(error, ERROR_CODE) {
const errorCodes = [ERROR_CODE.BANNER.NO_EXIST_BANNER];
if (errorCodes.includes(error.code)) {
console.error(error);
return true;
}
return false;
},
afterErrorAction(error, ERROR_CODE) {
if (
[
ERROR_CODE.MY_ORDER.NO_EXIST_ORDER,
ERROR_CODE.MY_ORDER.NO_MY_ORDER,
ERROR_CODE.MY_ORDER.NO_GUEST_MY_ORDER,
ERROR_CODE.CART.SOLDOUT_OPTION,
ERROR_CODE.CART.NO_EXHIBITION,
].includes(error.code)
) {
shopby.goHome();
}
if (
[
ERROR_CODE.CART.STOP_SALE,
ERROR_CODE.CART.NO_SALE,
ERROR_CODE.CART.WAIT_SALE,
ERROR_CODE.CART.NO_OPTION,
ERROR_CODE.CART.SOLDOUT_OPTION,
].includes(error.code)
) {
location.reload();
}
},
};
api.initiate();
shopby.cache = {
cacheData: [],
key: {
// token
accessToken: 'SHOPBYPRO_SHOP_TOKEN',
guestToken: 'SHOPBYPRO_GUEST_TOKEN',
recentlyKeyword: 'SHOPBYPRO_RECENTLY_KEYWORD',
accessNotIntroPath: 'SHOPBYPRO_ACCESS_NOT_INTRO_PATH',
// page
cart: {
guestInfo: 'SHOPBYPRO_CART_GUEST_INFO',
count: 'SHOPBYPRO_CART_COUNT',
},
order: {
cartInfo: 'SHOPBYPRO_CART_INFO',
cartCount: 'SHOPBYPRO_CART_COUNT',
},
member: {
saveMemberId: 'SHOPBYPRO_SAVE_MEMBER_ID',
oauthProvider: 'SHOPBYPRO_OAUTH_PROVIDER',
oauthToken: 'SHOPBYPRO_OAUTH_CSRF',
kcpAuth: 'SHOPBYPRO_KCP_AUTH',
dormant: 'SHOPBYPRO_DORMANT',
},
product: {
recent: 'RECENT-PRODUCTS',
},
// designPopups
designPopups: {
popups: 'SHOPBYPRO_DESIGN_POPUPS',
invisibleToday: 'SHOPBYPRO_INVISIBLE_TODAY',
},
},
dataKey: {
mall: 'mall',
mallSSLSeal: 'mallSSLSeal',
categories: 'categories',
skin: 'skin',
boardsConfig: 'boardsConfig',
footerArticles: 'footerArticles',
footerAbout: 'footerAbout',
orderConfigs: 'orderConfigs',
pageScripts: 'pageScripts',
externalScripts: 'externalScripts',
skinGroup: 'skinGroup',
profile: 'profile',
},
async fetchDataWithCaching(api, name, parameter = null, cacheTime = 0) {
const { data } = await api(parameter);
shopby.localStorage.setItemWithExpire(name, data, cacheTime);
},
async checkMallDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.mall);
if (!dueData) await this.fetchMalls();
},
fetchMalls() {
const { profile, clientId } = shopby.config.skin;
const cdnUri = encodeURI(
`https://rlgkd0v7e.toastcdn.net/mall-configurations/${profile}/${encodeURIComponent(clientId)}/mallInfo.js`,
);
const dataKey = this.dataKey.mall;
const deferred = $.Deferred();
// jsonp 데이터타입은 fetch 에서 제공하지 않기 때문에 jquery 를 사용함.
$.ajax({
url: cdnUri,
jsonpCallback: 'getMalls', // api 담당자와 협의된 이름입니다. 변경이 필요한 경우 BE 운영관리팀에 문의 후 변경해주세요.
dataType: 'jsonp',
success: function (malls) {
// getMalls api 를 호출하기 전에 cdn 에 해당 몰의 설정이 존재하는지 먼저 확인
shopby.localStorage.setItemWithExpire(dataKey, malls);
deferred.resolve();
},
error: $.proxy(function () {
// cdn 에 없는 경우 getMalls api 를 직접 호출
deferred.resolve(this.fetchDataWithCaching(shopby.api.admin.getMalls, dataKey));
}, this),
});
return deferred.promise();
},
async checkMallSSLSealDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.mallSSLSeal);
if (!dueData) await this.fetchDataWithCaching(shopby.api.admin.getMallsSsl, this.dataKey.mallSSLSeal);
},
async checkSkinGroupDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.skinGroup);
if (!dueData)
await this.fetchDataWithCaching(shopby.api.display.getSkinBannersGroupsBySkin, this.dataKey.skinGroup, {
queryString: { isPreview: false },
});
},
async checkCategoriesDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.categories);
if (!dueData) await this.fetchDataWithCaching(shopby.api.display.getCategories, this.dataKey.categories);
},
async checkBoardsConfigDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.boardsConfig);
if (!dueData) await this.fetchDataWithCaching(shopby.api.manage.getBoardsConfig, this.dataKey.boardsConfig);
},
async checkFooterArticlesDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.footerArticles);
if (!dueData)
await this.fetchDataWithCaching(shopby.api.manage.getBoardsBoardNoArticles, this.dataKey.footerArticles, {
pathVariable: {
boardNo: 'notice',
},
queryString: {
hasTotalCount: false,
searchType: 'ALL',
withReplied: false,
page: 1,
pageSize: 5,
},
});
},
async checkFooterAboutDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.footerAbout);
if (!dueData)
await this.fetchDataWithCaching(shopby.api.manage.getTerms, this.dataKey.footerAbout, {
queryString: {
termsTypes: 'MALL_INTRODUCTION,ACCESS_GUIDE',
},
});
},
async checkSkinDataExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.skin);
const search = new URLSearchParams(window.location.search);
const skinNo = search.get('skinNo') || '';
if (!dueData) {
const bannerGroupCodes = shopby.config.skin.bannerGroupCodes.join(',');
if (!bannerGroupCodes) return;
// const skinCode = shopby.config.skin.skinCode;
const request = {
queryString: {
bannerGroupCodes,
// skinCode,
},
};
if (skinNo) {
request.queryString.skinNo = skinNo;
}
await this.fetchDataWithCaching(shopby.api.display.getSkinBanners, this.dataKey.skin, request, 0);
}
},
async checkOrderConfigExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.orderConfigs);
if (!dueData) {
await this.fetchDataWithCaching(shopby.api.order.getOrderConfigs, this.dataKey.orderConfigs);
}
},
async checkPageScriptsExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.pageScripts);
if (!dueData) {
const request = {
queryString: {
pageTypes:
'MAIN, COMMON_HEAD, COMMON_FOOTER, PRODUCT, PRODUCT_LIST, PRODUCT_SEARCH, DISPLAY_SECTION, CART, ORDER, ORDER_DETAIL, ORDER_COMPLETE, MEMBER_JOIN_COMPLETE',
},
};
await this.fetchDataWithCaching(shopby.api.manage.getPageScripts, this.dataKey.pageScripts, request);
}
},
async checkExternalScriptExpired() {
const dueData = shopby.localStorage.getItemWithExpire(this.dataKey.externalScripts);
if (!dueData) {
await this.fetchDataWithCaching(shopby.api.workspace.getExternalScripts, this.dataKey.externalScripts);
}
},
async checkProfileSession() {
const dueData = shopby.sessionStorage.getItemWithExpire(this.dataKey.profile);
if (!dueData) {
const { data } = await shopby.api.member.getProfile();
shopby.sessionStorage.setItemWithExpire(this.dataKey.profile, data);
}
},
getMall() {
return shopby.localStorage.getItem(this.dataKey.mall);
},
getMallsSSLSeal() {
return shopby.localStorage.getItem(this.dataKey.mallSSLSeal);
},
getSkinGroup() {
return shopby.localStorage.getItem(this.dataKey.skinGroup);
},
getCategories() {
return shopby.localStorage.getItem(this.dataKey.categories);
},
getBoardsConfig() {
const config = shopby.localStorage.getItem(this.dataKey.boardsConfig);
const boardConfigKeysForPro = ['inquiryConfig', 'productInquiryConfig', 'productReviewConfig'];
return boardConfigKeysForPro.reduce(
(boardConfig, key) => ({
...boardConfig,
[key]: {
used: false,
name: '',
...config[key],
},
}),
config,
);
},
getFooterArticles() {
return shopby.localStorage.getItem(this.dataKey.footerArticles);
},
getFooterAbout() {
return shopby.localStorage.getItem(this.dataKey.footerAbout);
},
getSkin() {
return shopby.localStorage.getItem(this.dataKey.skin);
},
getOrderConfigs() {
return shopby.localStorage.getItem(this.dataKey.orderConfigs);
},
getPageScripts() {
return shopby.localStorage.getItem(this.dataKey.pageScripts);
},
getExternalScripts() {
return shopby.localStorage.getItem(this.dataKey.externalScripts);
},
setAccessToken(accessToken, expireSeconds) {
shopby.localStorage.setItemWithExpire(shopby.cache.key.accessToken, accessToken, expireSeconds * 1000);
},
getAccessToken() {
const accessToken = shopby.localStorage.getItemWithExpire(shopby.cache.key.accessToken);
return accessToken ? accessToken : '';
},
removeAccessToken() {
shopby.localStorage.removeItem(shopby.cache.key.accessToken);
shopby.localStorage.removeItem(shopby.cache.key.member.oauthProvider);
shopby.localStorage.removeItem(shopby.cache.key.member.oauthToken);
shopby.sessionStorage.removeItem(shopby.cache.dataKey.profile);
},
setGuestToken(guestToken) {
return shopby.localStorage.setItem(shopby.cache.key.guestToken, guestToken);
},
getGuestToken() {
const guestToken = shopby.localStorage.getItem(shopby.cache.key.guestToken);
return guestToken ? guestToken : '';
},
async getRecentProducts() {
const cacheKey = shopby.cache.key.product.recent;
const key = shopby.logined() ? cacheKey : `GUEST-${cacheKey}`;
let products = shopby.localStorage.getItemWithExpire(key) || [];
if (shopby.utils.isArrayEmpty(products)) {
const { data } = await shopby.api.product.getProfileRecentProducts({ queryString: {} });
shopby.localStorage.setItemWithExpire(key, data, 60 * 5 * 1000);
}
return products;
},
};
})();
})();
/**
* 전역 로직.
*/
(() => {
const core = {
mallName: null,
GLOBAL_VARIABLE_NAME_SPACE: 'sb',
async initiate(pageInitiate) {
await this.setEnvironmentData();
await this.setSkinGroup();
await this.prepareGlobalData();
this.setMall();
if (pageInitiate) shopby.start.entries.push(pageInitiate);
this.setMetaInfo();
this.setGATC();
shopby.setGlobalVariableBy = this.setGlobalVariableBy.bind(this);
this.setStatisticsLogging();
this.setDefaultGlobalVariable();
this.appendExternalScript('COMMON_HEAD');
/* 하단 공통 스크립트 엔트리에 추가 */
shopby.start.entries.push(() => this.appendExternalScript('COMMON_FOOTER'));
this.renderGlobalPartials();
this.startEntries();
await this.redirectIntro();
},
async environmentInfo() {
const urlInfo = {
api: {
alpha: 'https://alpha-shop-api.e-ncp.com',
real: 'https://shop-api.e-ncp.com',
},
storage: {
alpha: 'https://alpha-storage.e-ncp.com',
real: 'https://storage.e-ncp.com',
},
};
const result = await fetch('/environment.json');
const { clientId, profile } = await result.json();
return {
clientId,
profile,
apiUrl: urlInfo.api[profile],
storageApiUrl: urlInfo.api[profile],
};
},
async setEnvironmentData() {
const { clientId, profile, apiUrl, storageApiUrl } = await this.environmentInfo();
shopby.config.skin.clientId = clientId;
shopby.config.skin.profile = profile;
shopby.config.apiUrl = apiUrl;
shopby.config.storageApiUrl = storageApiUrl;
},
async setSkinGroup() {
const search = new URLSearchParams(window.location.search);
const skinNo = search.get('skinNo') || '';
const isPreview = !!skinNo;
let skinGroup;
if (isPreview) {
const request = {
queryString: { isPreview: true, skinNo },
};
const { data } = await shopby.api.display.getSkinBannersGroupsBySkin(request);
skinGroup = data ? data : {};
} else {
await shopby.cache.checkSkinGroupDataExpired();
skinGroup = shopby.cache.getSkinGroup();
}
shopby.config.skin.platformType = skinGroup.platformType;
shopby.config.skin.bannerGroups = skinGroup.bannerGroups;
if (!skinGroup.bannerGroups) return;
for (const banner of skinGroup.bannerGroups) {
shopby.config.skin.bannerGroupCodes.push(banner.groupCode);
}
},
setMall() {
const { mall } = shopby.cache.getMall();
this.mallName = mall.mallName;
shopby.config.skin.mallName = mall.mallName;
},
setTitle() {
document.title = this.mallName;
},
setBasicInfo() {
const names = ['author', 'description', 'keywords'];
names.forEach(name => {
$(`meta[name=${name}]`).prop('content', this.mallName);
});
},
setOgInfo() {
const image = new Image();
image.src = '/assets/img/banner/top-logo.png';
$("meta[property='og:title']").prop('content', this.mallName);
$("meta[property='og:url']").prop('content', location.href);
$("meta[property='og:image:width']").prop('content', image.width);
$("meta[property='og:image:height']").prop('content', image.height);
},
setTwitterInfo() {
$("meta[name='twitter:title']").prop('content', this.mallName);
},
async initializeNaverShoppingConfiguration() {
const { naverWebmaster } = shopby.cache.getMall().externalServiceConfig;
const { data } = await shopby.api.product.getProductsConfigurationNaverShopping();
shopby.naverInflow.setNaverWebmaster(naverWebmaster);
shopby.naverInflow.setNaverShoppingConfiguration(data);
shopby.naverInflow.setExecuteScript();
},
async setMetaInfo() {
this.setTitle();
this.setBasicInfo();
this.setOgInfo();
this.setTwitterInfo();
this.initializeNaverShoppingConfiguration();
},
setGATC() {
const externalServiceConfig = shopby.cache.getMall().externalServiceConfig;
if (!externalServiceConfig) return;
const gtagId = shopby.cache.getMall().externalServiceConfig.googleAnalytics;
const gtagScript = `
`;
$(document.head).append(gtagScript);
},
setStatisticsLogging() {
const SCRIPT_URL = 'https://rl3flznkr.toastcdn.net/shopby-statistics-recorder.js';
const scriptElement = document.createElement('script');
scriptElement.async = true;
scriptElement.src = SCRIPT_URL;
document.head.append(scriptElement);
scriptElement.addEventListener('load', () => {
const { clientId } = shopby.config.skin;
const memberNo = sb.profile ? sb.profile.memberNo.toString() : '';
const { profile } = shopby.config.skin;
if (typeof window.shopbyStatistics === 'function') {
const statParams = {
clientId,
memberNo,
};
if (profile === 'alpha') {
statParams.profile = profile;
}
window.shopbyStatistics(statParams);
}
});
},
appendExternalScript(currentPageType) {
if (!currentPageType) return;
const adminPageScripts = shopby.cache.getPageScripts() || []; // 어드민 설정 스크립트
const externalScripts = shopby.cache.getExternalScripts() || []; // 외부 앱 스크립트
const pageScripts = [...adminPageScripts, ...externalScripts];
const commonScripts = pageScripts.filter(
({ pageType, deviceType }) => pageType === currentPageType && deviceType === 'PC',
);
const tagKey = currentPageType.includes('HEAD') ? 'head' : 'body';
const shopbyMetaList = document.querySelectorAll('meta');
const validMetaTagProps = ['name', 'property'];
const getDeletedMetaTags = script => {
const domParser = new DOMParser();
const vDom = domParser.parseFromString(script.content, 'text/html');
const vMeta = vDom.querySelectorAll('meta');
const replaceableMetaTags = Array.from(vMeta).filter(meta =>
validMetaTagProps.some(propName => meta.hasAttribute(propName)),
);
const isMetaMatched = (el, propName) =>
el.hasAttribute(propName) &&
replaceableMetaTags.some(metaTag => el.getAttribute(propName) === metaTag.getAttribute(propName));
return Array.from(shopbyMetaList).filter(shopbyMeta =>
validMetaTagProps.some(propName => isMetaMatched(shopbyMeta, propName)),
);
};
commonScripts.forEach(script => {
if (tagKey === 'head') {
const deletedMetaTags = getDeletedMetaTags(script);
deletedMetaTags.forEach(metaTag => {
document.head.removeChild(metaTag);
});
}
$(document[tagKey]).append(script.content);
});
},
async prepareGlobalData() {
const entries = [
shopby.cache.checkMallDataExpired(),
shopby.cache.checkMallSSLSealDataExpired(),
shopby.cache.checkCategoriesDataExpired(),
shopby.cache.checkSkinDataExpired(),
shopby.cache.checkBoardsConfigDataExpired(),
shopby.cache.checkOrderConfigExpired(),
shopby.cache.checkPageScriptsExpired(),
shopby.cache.checkExternalScriptExpired(),
];
if (shopby.logined()) {
entries.push(shopby.cache.checkProfileSession());
}
await Promise.all(entries);
},
renderGlobalPartials() {
$('#header').load('/components/header.html');
$('#footer').load('/components/footer.html');
$('#aside').load('/components/aside.html');
},
startEntries() {
shopby.start.entries.forEach(initiate => initiate());
},
setGlobalVariableNamespace() {
window[this.GLOBAL_VARIABLE_NAME_SPACE] = window[this.GLOBAL_VARIABLE_NAME_SPACE] || {};
},
setDefaultGlobalVariable() {
this.setGlobalVariableNamespace();
window[this.GLOBAL_VARIABLE_NAME_SPACE]['getPlatform'] =
window[this.GLOBAL_VARIABLE_NAME_SPACE]['getPlatform'] || shopby.utils.getPlatform;
window[this.GLOBAL_VARIABLE_NAME_SPACE]['profile'] = shopby.sessionStorage.getItem(shopby.cache.dataKey.profile);
},
/**
* @param {'MAIN'|'PRODUCT'|'PRODUCT_SEARCH'|'PRODUCT_LIST'|'DISPLAY_SECTION'|'CART'|'ORDER'|'ORDER_COMPLETE'|'ORDER_DETAIL'} key
* @param {object} payload
*/
setGlobalVariableBy(key, payload) {
const pageVariableNameMap = {
MAIN: 'main',
PRODUCT: 'product',
PRODUCT_SEARCH: 'searchedProduct',
PRODUCT_LIST: 'searchedProduct',
DISPLAY_SECTION: 'displaySection',
CART: 'cart',
ORDER: 'orderSheet',
ORDER_COMPLETE: 'order',
ORDER_DETAIL: 'order',
MEMBER_JOIN_COMPLETE: null,
PROFILE_PRODUCT_REVIEW: 'profileProductReview',
PROFILE_INQUIRIES_PROGRESS: 'profileInquiriesProgress',
PROFILE_INQUIRIES_ANSWERED: 'profileInquiriesAnswered',
PROFILE_PRODUCT_INQUIRIES_PROGRESS: 'profileProductInquiriesProgress',
PROFILE_PRODUCT_INQUIRIES_ANSWERED: 'profileProductInquiresAnswered',
PROFILE_PRODUCT_REVIEWABLE: 'profileProductReviewable',
PROFILE_PRODUCT_REVIEWED: 'profileProductReviewed',
PROFILE_ORDERS_SUMMARY_STATUS: 'profileOrdersSummaryStatus',
};
this.setGlobalVariableNamespace();
window[this.GLOBAL_VARIABLE_NAME_SPACE][pageVariableNameMap[key]] =
payload instanceof Object ? JSON.parse(JSON.stringify(payload)) : payload;
this.appendExternalGlobalVariableScripts(key);
},
appendExternalGlobalVariableScripts(key) {
const scriptsHTMLTemplate = this.getExternalGlobalVariableScripts(key)
.map(({ content }) => content)
.join('');
if (scriptsHTMLTemplate) $(document.body).append(scriptsHTMLTemplate);
},
getExternalGlobalVariableScripts(key) {
const adminPageScripts = shopby.cache.getPageScripts();
const externalScripts = shopby.cache.getExternalScripts();
const platform = 'PC';
return [...adminPageScripts, ...externalScripts].filter(
({ pageType, deviceType }) => pageType === key && deviceType === platform,
);
},
async redirectIntro() {
const _introType = shopby.utils.getUrlParam('introType', null);
const introType = _introType ? _introType : shopby.cache.getMall().mall.introRedirection[shopby.platform];
const isIntroPage = location.pathname.includes('/intro') || location.pathname.includes('/callback');
const except = ['/join', '/login/login', '/find'];
const accessExceptPath = shopby.localStorage.getItem(shopby.cache.key.accessNotIntroPath) || false;
const hasExceptPath = except.some(url => location.pathname.includes(url));
const mustGoHome = (isIntroPage && accessExceptPath) || (hasExceptPath && shopby.logined());
const mustGoLogin =
location.pathname.includes('/my') &&
location.pathname !== '/pages/my/order.html' &&
location.pathname !== '/pages/my/order-claim.html' &&
location.pathname !== '/pages/my/previous-order.html' &&
!shopby.logined();
if (introType === 'NONE') {
shopby.localStorage.removeItem(shopby.cache.key.accessNotIntroPath);
}
if (mustGoLogin) {
if (introType === 'NONE') {
shopby.goLogin();
shopby.utils.hideAllContents();
}
return;
}
if (mustGoHome) {
if (introType === 'NONE') {
shopby.goHome();
}
return;
}
if (hasExceptPath) return;
if (introType === 'NO_ACCESS' && !isIntroPage) {
location.href = '/pages/intro/no-access.html';
}
if (introType === 'ONLY_MEMBER' && !shopby.logined() && !isIntroPage) {
shopby.localStorage.setItem(shopby.cache.key.accessNotIntroPath, true);
location.href = '/pages/intro/member-only.html';
}
if (introType === 'ONLY_ADULT') {
shopby.localStorage.setItem(shopby.cache.key.accessNotIntroPath, true);
// WARN : /pages/intro/adult-certification.html 리다이렉트 하면안되는 페이지도 잡아 챌 수 있음.
if (!shopby.logined() && !isIntroPage) {
location.href = '/pages/intro/adult-certification.html';
return;
}
if (!shopby.logined()) return;
const memberInfo = await shopby.api.member.getProfile();
const adultCertificatedYmdt = dayjs(memberInfo.data.adultCertificatedYmdt);
const today = dayjs();
const overdue = today.diff(adultCertificatedYmdt, 'year', true) > 1;
const allCertificated = memberInfo && memberInfo.data && memberInfo.data.adultCertificated && !overdue;
if (allCertificated && isIntroPage) {
shopby.goHome();
} else if (!allCertificated && !isIntroPage) {
location.href = '/pages/intro/adult-certification.html';
}
shopby.localStorage.removeItem(shopby.cache.key.accessNotIntroPath);
}
},
};
shopby.start.initiate = core.initiate.bind(core);
const global = {
initiate() {
this.curtainBanner.initiate();
this.layerPopup.initiate();
},
curtainBanner: {
data: {
content: null,
},
initiate() {
this.setData();
// if (this.data.content !== null) this.render();
},
setData() {
this.data.content = '커튼 배너';
},
render() {
const content = this.generateContent();
$(document.body).prepend(content);
},
generateContent() {
return `
`;
},
},
layerPopup: {
data: {
content: null,
},
initiate() {
this.setData();
// if (this.data.content !== null) this.render();
},
setData() {
this.data.content = '레이어 팝업';
},
render() {
const content = this.generateContent();
$(document.body).append(content);
},
generateContent() {
return `
`;
},
},
};
shopby.start.entries.push(global.initiate.bind(global));
console.log('%cSHOPBY', 'font-size:44px;color:#e63533;padding:2px 4px;border-radius:10px;background:#fff;');
console.log('%c© NHN COMMERCE Corp. All rights reserved.', 'font-size:25px;');
console.log(
'%c이 기능은 개발자용으로 브라우저에서 제공되는 내용입니다. 개발용도 이외에 스크립트를 조작하거나 유출한다면 제재를 받을 수 있습니다.',
'font-size:18px;',
);
})();
/**
* jQuery.fn 확장
*/
(() => {
const compliedTemplates = [];
/**
* 원본 태그에는 반드시 id 속성 필수!
* id가 아니라 selector 를 받아도 될듯함.
* 나중에 로딩바를 보여주고 show 시점에 로딩바 제거 기능이 필요할수도..
*
* @author Haekyu Cho
* @param data
*/
$.fn.render = function (data) {
this.renderTemplateWithRawHtml(data, this.html());
return this;
};
$.fn.renderTemplateWithRawHtml = function (data, html) {
const id = this.attr('id');
const templateId = this.attr('data-template-id'); // script text/x-handlebars-template 렌더시
if (!compliedTemplates[id]) {
compliedTemplates[id] = Handlebars.compile(templateId ? $(`#${templateId}`).html() : html);
}
templateId ? this.replaceWith(compliedTemplates[id](data)) : this.html(compliedTemplates[id](data)).show();
this.removeClass('invisible').addClass('visible');
};
$.fn.enterKeyup = function (selector) {
const callback = function (key) {
if (key.code === 'Enter' || key.code === 'NumpadEnter') {
this.click();
}
};
$(selector).on('keyup', callback.bind(this));
};
$.fn.visualize = function () {
this.removeClass('invisible').addClass('visible');
};
})();
// @fixme: 나중에 옮기자!
(() => {
// imageUrl[0] 용으로 사용중..
// 확장하려면 haekyu.cho에게 문의하세요.
// key가 first일 경우도 있어서 prefix를 두는것도 방법일듯함.
Handlebars.registerHelper({
getFirstImageWithDefault(array) {
return shopby.utils.isArrayNotEmpty(array) ? array[0] : '//rlyfaazj0.toastcdn.net/no_img.png';
},
toCurrencyString(number = 0) {
return shopby.utils.toCurrencyString(number);
},
toSignedCurrencyString(number = 0) {
return shopby.utils.toCurrencyString(number, 'always');
},
hasParam(value, options) {
return value ? options.fn(this) : options.inverse(this);
},
ifLogined: function (options) {
if (shopby.logined()) {
return options.fn(this);
}
return options.inverse(this);
},
ifGt(x, y, options) {
return x > y ? options.fn(this) : options.inverse(this);
},
ifGe(x, y, options) {
return x >= y ? options.fn(this) : options.inverse(this);
},
ifLt(x, y, options) {
return x < y ? options.fn(this) : options.inverse(this);
},
ifLe(x, y, options) {
return x <= y ? options.fn(this) : options.inverse(this);
},
ifEq(x, y, options) {
return x === y ? options.fn(this) : options.inverse(this);
},
ifNe(x, y, options) {
return x !== y ? options.fn(this) : options.inverse(this);
},
ifEqOne(a, x, y, options) {
return a === x || a === y ? options.fn(this) : options.inverse(this);
},
ifEqOneOfThree(a, x, y, z, options) {
return a === x || a === y || a === z ? options.fn(this) : options.inverse(this);
},
substring(value, start, end) {
return value.substring(start, end);
},
increment(index) {
return index + 1;
},
listIndex(index, pageNumber, pageSize) {
return (pageNumber - 1) * pageSize + (index + 1);
},
ifHasDiscount(immediateDiscountAmt, additionDiscountAmt, options) {
return immediateDiscountAmt + additionDiscountAmt > 0 ? options.fn(this) : options.inverse(this);
},
option(value, label, selectedValue, disabled) {
const selectedProperty = value === selectedValue ? 'selected="selected"' : '';
const disabledProperty = typeof disabled !== 'object' && disabled ? 'disabled' : '';
return new Handlebars.SafeString(
``,
);
},
checked(value, checkedValue, actualValue = '') {
const resultValue = actualValue && typeof actualValue === 'string' ? actualValue : value;
return `value=${resultValue} ${checkedValue === value ? 'checked' : ''}`;
},
sum(...nums) {
return nums.filter(num => typeof num === 'number').reduce((a, b) => a + b);
},
/*
* new : 등록시간(registerYmdt)이 24시간 이내인 경우
* hot : 조회 수(viewCount)가 100 이상인 경우
*
* @author BoMee Yoon
* @param className : 'new' | 'hot'
* @param conditionInfo : 'registerYmdt' | 'viewCount'
* */
badge(className, conditionInfo) {
let text = '';
let needsTag = false;
switch (className) {
case 'new':
needsTag = dayjs().diff(conditionInfo, 'hour') <= 24;
text = 'N';
break;
case 'hot':
needsTag = conditionInfo >= 100;
text = 'HOT';
break;
default:
break;
}
return needsTag ? new Handlebars.SafeString(`${text}`) : '';
},
img(url, width, height, title = '', className = '', style = '') {
if (shopby.utils.isArrayNotEmpty(url)) {
url = url[0];
}
let size = '';
if (width !== undefined && typeof width !== 'number') {
width = 0;
}
if (width) {
size = `?${width}x${height}`;
width = `width="${width}"`;
}
let escaped = Handlebars.escapeExpression(url);
if (escaped.includes('http://')) {
escaped = escaped.replace('http:', '');
}
return new Handlebars.SafeString(`
`);
},
safeString(text) {
return text ? new Handlebars.SafeString(text) : '';
},
getOptionNameAndValue(optionName, optionValue) {
const separator = '|';
const names = optionName.split(separator);
const values = optionValue.split(separator);
const totalCount = names.length;
if (totalCount === 0) return '';
return names.reduce((acc, curr, idx) => {
const isLast = idx === totalCount - 1;
return (acc += `${curr}: ${values[idx]}${isLast ? '' : ` ${separator} `}`);
}, '');
},
//@todo getFirstImageWithDefault 랑 동일함. getFirstImageWithDefault사용되지 않고 있음. 추후 변경
getFirstItem(array) {
return shopby.utils.isArrayNotEmpty(array) ? array[0] : '';
},
setOrderStateValue(type) {
return {
DEPOSIT_WAIT: '입금대기',
PAY_DONE: '결제완료',
PRODUCT_PREPARE: '상품준비중',
DELIVERY_PREPARE: '배송준비중',
DELIVERY_ING: '배송중',
DELIVERY_DONE: '배송완료',
BUY_CONFIRM: '구매확정',
CANCEL_DONE: '취소완료',
RETURN_DONE: '반품완료',
EXCHANGE_DONE: '교환완료',
PAY_WAIT: '결제대기',
PAY_CANCEL: '결제포기',
PAY_FAIL: '결제실패',
DELETE: '삭제',
EXCHANGE_WAIT: '교환대기',
REFUND_DONE: '환불완료',
PRODUCT: '상품',
DELIVERY: '배송',
CANCEL: '취소',
RETURN: '반품',
EXCHANGE: '교환',
REFUND: '환불',
OTHER: '기타',
}[type];
},
hasUserInfo: function (profile, options) {
const hasRequiredInfo = () => {
const _profile = profile || {};
return _profile.memberName || _profile.email || _profile.mobileNo || _profile.sex || _profile.birthday;
};
return hasRequiredInfo() ? options.fn(this) : options.inverse(this);
},
setProductInquiryTypesValue(type) {
return {
PRODUCT: '상품',
DELIVERY: '배송',
CANCEL: '취소',
RETURN: '반품',
EXCHANGE: '교환',
REFUND: '환불',
OTHER: '기타',
}[type];
},
arrayJoin: function (array, separator) {
return array.join(separator);
},
getPlainText(htmlString) {
return htmlString.replace(/<[^>]+>/g, '');
},
});
})();
(() => {
const child = [];
const getChildPopup = name => {
return new Promise(resolve => {
if (child[name]) {
resolve(child[name]);
return;
}
$('#popups-area').append(
$('').load(`/components/popup/${name}.html`, () => {
resolve(child[name]);
}),
);
});
};
const close = function (status) {
this.$el.remove();
$('body').removeClass('popup-open');
status = status || { state: 'close' };
if (this.callback) {
this.callback(status);
}
};
const closeButtonEvent = function (event) {
const actionType = $(event.currentTarget).data('action-type') || 'negative';
let status;
switch (actionType) {
case 'positive':
status = { state: 'ok', result: true };
break;
case 'updated':
status = { state: 'updated' };
break;
default:
status = undefined;
}
this.close(status);
};
/**
* 반드시 팝업 공통으로 들어가야 하는 로직 & function & event 추가
* - close 함수 추가
* - 우상단 x, 확인, 취소 버튼 이벤트 추가
*
* @author Daejoong Son
* @param instance 팝업 인스턴스
*/
const addPopupFunc = instance => {
// close 함수 추가
instance.close = close.bind(instance);
// 우상단 x, 확인, 취소 버튼 이벤트 추가
instance.$el.on('click', '.btnClosePopup', closeButtonEvent.bind(instance));
};
/**
* 팝업 생성자 등록
* - 동적 로딩된 팝업 생성자를 등록
* - 팝업 재호출시 다시 동적 로딩하지 않고 곧바로 팝업 생성자를 사용하기 위해
*
* @author Daejoong Son
* @param name 팝업명 - 로드하려는 팝업 파일 이름 -> `/components/popup/${name}.html` 과 맞춘다
* @param childPopup 등록하려는 팝업 생성자
*/
shopby.registerPopupConstructor = (name, childPopup) => {
if (!child[name]) {
child[name] = childPopup;
}
};
/**
* 팝업 호출
* - 동적 로딩 & 인스턴스 생성
*
* @author Daejoong Son
* @param name 팝업명 - 로드하려는 팝업 파일 이름 -> `/components/popup/${name}.html` 과 맞춘다
* @param option 팝업에 전달될 옵션 데이터 - 각 팝업마다 필요한 데이터가 있으면 여기로 전달
* @param callback 팝업에서 action 발생시 호출되는 callback 함수. 기본적으로 {state: '상태값'} 를
* 전달하며 필요시 property 를 추가한다.
*/
shopby.popup = async (name, option, callback) => {
if (!name) {
console.error('null popup name...');
return;
}
const childPopup = await getChildPopup(name);
if (!childPopup) {
console.error('fail get popup class...');
return;
}
const instance = new childPopup($('#popups-area'), option, callback);
if (!instance) {
console.error('fail create popup instance...');
return;
}
addPopupFunc(instance);
$('body').addClass('popup-open');
};
shopby.alert = (option, callback) => {
option = option || {};
if (!Object.prototype.hasOwnProperty.call(option, 'message')) {
option = {
message: option.message || option,
};
}
shopby.popup('message', option, callback);
};
shopby.confirm = (option, callback) => {
option = option || {};
option.yesOrNo = true;
shopby.popup('message', option, callback);
};
})();
(() => {
class Pagination {
constructor(callback, selector, pageSize = 10, scrollToTop = true) {
this.pageNumber = 1;
this.pageSize = pageSize;
this.totalCount = 0;
this.callback = callback;
this.scrollToTop = scrollToTop;
this.$page = $(selector);
this.$page.append(this.template);
this.bindEvents();
this.totalPage = 0;
}
render(totalCount = 0, pageNumber) {
this.pageNumber = pageNumber || this.pageNumber;
this.totalCount = totalCount;
this.totalPage = this.getTotalPage();
this.renderPageNumber();
this.$page.addClass('visible');
}
bindEvents() {
this.$page.on('click', 'li:not(.on)', this.onClickPage.bind(this));
}
onClickPage(e) {
e.preventDefault();
const dataset = e.currentTarget.dataset;
switch (dataset.type) {
case 'page': {
this.pageNumber = Number(dataset.pageNumber);
break;
}
case 'nextPage': {
if (this.pageNumber < this.totalPage) {
this.pageNumber += 1;
}
break;
}
case 'prevPage': {
if (this.pageNumber > 1) {
this.pageNumber -= 1;
}
break;
}
case 'firstPage': {
const canPrev = this.pageNumber !== 1;
if (canPrev) {
// this.pageNumber = (Math.floor(this.pageNumber / this.pageSize) - 1) * this.pageSize + 1;
//
// if (this.pageNumber <= 0) {
// this.pageNumber = 1;
// }
this.pageNumber = 1;
}
break;
}
case 'lastPage': {
const end = this.totalPage;
const canNext = this.pageNumber !== end;
if (canNext) {
// this.pageNumber = Math.ceil(this.pageNumber / this.pageSize) * this.pageSize + 1;
//
// if (this.pageNumber > end) {
// this.pageNumber = end;
// }
this.pageNumber = end;
}
break;
}
default:
return;
}
this.scrollToTop && window.scrollTo(0, 0);
this.callback();
}
renderPageNumber() {
const PAGE_LIMIT = 10;
const getStartNumber = () => parseInt((this.pageNumber - 1) / PAGE_LIMIT) * PAGE_LIMIT + 1;
const range = (length, num) => Array.from({ length }, (_, i) => i + num);
const pageBtnHtml = range(PAGE_LIMIT, getStartNumber()).map(item => {
const className = item === this.pageNumber ? 'on' : '';
if (item > this.totalPage) return;
return `${item}`;
});
this.$page.find('li[data-type=page]').remove();
this.$page.find('li.btn_page_prev').after(pageBtnHtml);
}
get template() {
return `
`.trim();
}
getTotalPage() {
return Math.ceil(this.totalCount / this.pageSize) || 1;
}
}
shopby.pagination = Pagination;
})();
$(() => {
class DateRangePicker {
constructor(
selector,
searchCallBack,
maxRange = null,
start = this.start,
end = this.end,
rangeKeyword = this.rangeKeyword,
dateButtons = this.DATE_BUTTONS,
) {
this.selector = selector;
this.rangePicker = {};
this.maxMonth = maxRange && maxRange.month;
this.maxLabel = maxRange && maxRange.label;
this.dateButtons = dateButtons;
this.prevStart = '';
this.prevEnd = '';
const isDefault = !(this.start || this.end || this.rangeKeyword);
if (isDefault) {
this.dateQueryInit(shopby.date.beforeToday(7), shopby.date.today(), 'week');
} else {
this.dateQueryInit(start, end, rangeKeyword);
}
// this.validate(start, end);
this.render();
this.createDatePicker();
this.bindEvents(searchCallBack);
}
get start() {
return shopby.utils.getUrlParam('startYmd');
}
get end() {
return shopby.utils.getUrlParam('endYmd');
}
get rangeKeyword() {
return shopby.utils.getUrlParam('rangeKeyword');
}
get DATE_BUTTONS() {
return {
day: {
label: '오늘',
value: '0',
unit: 'day',
},
week: {
label: '7일',
value: '-7',
unit: 'day',
},
day15: {
label: '15일',
value: '-15',
unit: 'day',
},
month: {
label: '1개월',
value: '-1',
unit: 'month',
},
month3: {
label: '3개월',
value: '-3',
unit: 'month',
},
year: {
label: '1년',
value: '-1',
unit: 'year',
},
};
}
setStart(startYmd) {
shopby.utils.replaceState({ startYmd });
}
setEnd(endYmd) {
shopby.utils.replaceState({ endYmd });
}
setRangeKeyword(rangeKeyword) {
shopby.utils.replaceState({ rangeKeyword });
}
removeRangeKeyword() {
const query = shopby.utils.getUrlParams();
delete query.rangeKeyword;
shopby.utils.replaceState(query);
}
dateQueryInit(startYmd, endYmd, rangeKeyword = null) {
this.setStart(startYmd);
this.setEnd(endYmd);
if (rangeKeyword) this.setRangeKeyword(rangeKeyword);
}
render() {
$(this.selector).append(getRow()).find('.date_check_list').append(this._makeDateButtonHtml());
$(this.selector).addClass('visible');
}
bindEvents(searchCallBack) {
// 조회기간 버튼 이벤트
$(this.selector).on('click', 'button[name="datePeriodBtn"][data-value]', event => {
this._setDateWithButton(event.target.dataset.value);
$(event.target).addClass('on').siblings().removeClass('on');
});
const throttleSearch = shopby.utils.throttle(searchCallBack, 500);
$(this.selector).on('click', '#searchDateRange', throttleSearch);
}
createDatePicker() {
const $startInput = $(this.selector).find('input[name=startDate]');
const $endInput = $(this.selector).find('input[name=endDate]');
const rangePicker = new tui.DatePicker.createRangePicker({
startpicker: {
input: $startInput[0],
container: $startInput.next()[0],
date: dayjs(this.start).toDate(),
},
endpicker: {
input: $endInput[0],
container: $endInput.next()[0],
date: dayjs(this.end).toDate(),
},
type: 'date',
format: 'yyyy-MM-dd',
language: 'ko',
});
this.rangePicker = rangePicker;
rangePicker.on('change:start', this._onChangeDatePicker.bind(this));
rangePicker.on('change:end', this._onChangeDatePicker.bind(this));
this._onChangeDatePicker(false);
}
_onChangeDatePicker(resetKeyword = true) {
const startDate = this.rangePicker.getStartDate();
const endDate = this.rangePicker.getEndDate();
let start = shopby.date.yymmdd(startDate);
let end = shopby.date.yymmdd(endDate);
const afterMonth = dayjs(start).add(this.maxMonth, 'month').format();
const diff = dayjs(afterMonth).diff(dayjs(end), 'day');
if (this.maxMonth !== null && diff < 0) {
alert(`조회기간은 최대 ${this.maxLabel} 까지 가능합니다.`);
start = this.prevStart;
end = this.prevEnd;
}
this.prevStart = start;
this.prevEnd = end;
this.setStart(start);
this.setEnd(end);
if (resetKeyword) {
$('button[name="datePeriodBtn"]').removeClass('on');
this.removeRangeKeyword();
}
this._setDatePicker(this.start, this.end);
}
_setDatePicker(startDate, endDate, isSetRange = true) {
const YEAR_LIMIT = 5; // 몇 년 까지 선택되게 할지에 대한 상수. 추후 필요하다면 constructor 인자로 승격
const startRange = [[dayjs(endDate).add(-YEAR_LIMIT, 'year'), endDate]];
const endRange = [[startDate, dayjs()]];
if (isSetRange === true) {
this.rangePicker.getStartpicker().setRanges(startRange);
this.rangePicker.getEndpicker().setRanges(endRange);
}
this.rangePicker.setEndDate(dayjs(endDate).toDate());
this.rangePicker.setStartDate(dayjs(startDate).toDate());
shopby.utils.pushState({
pageNumber: 1,
});
this.drawInputValue();
}
// 선택된 버튼의 값에 의하여 시작, 종료일을 셋팅한다.
_setDateWithButton(rangeKeyword) {
this.setRangeKeyword(rangeKeyword);
const { value, unit } = this.dateButtons[rangeKeyword];
const today = shopby.date.today();
this.setStart(dayjs(today).add(value, unit).format('YYYY-MM-DD'));
this.setEnd(today);
this._setDatePicker(this.start, this.end, false);
}
// 시작일, 종료일 input에 value 저장
drawInputValue() {
const $el = $(this.selector);
$el.find('input[name=startDate]').val(this.start);
$el.find('input[name=endDate]').val(this.end);
}
// once
_makeDateButtonHtml() {
let btnHtml = '';
for (const key in this.dateButtons) {
const className = key === this.rangeKeyword ? 'on' : '';
btnHtml += ``;
}
return btnHtml;
}
validate() {
if (!Object.prototype.hasOwnProperty.call(this.dateButtons, this.rangeKeyword)) {
throw new Error(
`기본 버튼값의 입력이 잘못되었습니다.\n 입력 버튼값 : [${Object.keys(this.dateButtons).join(
',',
)}], 잘못된 값 : ${this.rangeKeyword}`,
);
}
}
getClaimOrderRequestType() {
const claimOrderRequestType = $(this.selector).find('.claim-category input:checked').data('orderRequestType');
return claimOrderRequestType
? claimOrderRequestType
: 'CANCEL_DONE,CANCEL_PROCESSING,RETURN_DONE,RETURN_PROCESSING,EXCHANGE_DONE,EXCHANGE_PROCESSING';
}
}
const getRow = () => {
return `
${
location.href.includes('claims.html')
? `
`
: ''
}
`.trim();
};
shopby.dateRange = DateRangePicker;
});
(() => {
const _YYYYMMDD = 'YYYY-MM-DD';
shopby.date = {
today() {
return dayjs().format(_YYYYMMDD);
},
beforeToday(day = 0) {
return dayjs().subtract(day, 'day').format(_YYYYMMDD);
},
lastHalfYear() {
return dayjs().subtract(6, 'month').format(_YYYYMMDD);
},
yesterday() {
return dayjs().subtract(1, 'day').format(_YYYYMMDD);
},
lastWeek() {
return dayjs().subtract(1, 'week').format(_YYYYMMDD);
},
lastYear() {
return dayjs().subtract(1, 'year').format(_YYYYMMDD);
},
yymmdd(date) {
return dayjs(date).format(_YYYYMMDD);
},
isBefore(comparisonYmd, asOfYmd = null) {
return dayjs(asOfYmd ? asOfYmd : this.today()).isBefore(dayjs(comparisonYmd).format(_YYYYMMDD));
},
};
})();
/**
* 장바구니 데이터 헬퍼
*
* @author Daejoong Son
*/
(() => {
let cartData = null;
const defaultPrice = {
buyAmt: 0,
accumulationAmtWhenBuyConfirm: 0,
standardAmt: 0,
discountAmt: 0,
totalDeliveryAmt: 0,
totalPrePaidDeliveryAmt: 0,
totalPayOnDeliveryAmt: 0,
totalAmt: 0,
};
/**
* 카트의 중복 옵션 체크
*
* @param cart
* @param carts
* @return {boolean|*}
* @private
*/
const _isDuplicateCart = (cart, carts) => {
const itemOptionInputs = JSON.stringify(cart.option.optionInputs || []);
return carts
.filter(tempCart => cart !== tempCart)
.some(tempCart => {
if (cart.option.optionNo !== tempCart.option.optionNo) {
return false;
}
const tempItemOptionInputs = JSON.stringify(tempCart.option.optionInputs || []);
return itemOptionInputs === tempItemOptionInputs;
});
};
/**
* 구매제한 수량 관련
*
* @param cart
* @returns {boolean}
* @private
*/
const _isLimitations = cart => {
if (
cart.product.minBuyCount > 0 ||
cart.product.maxBuyCountInfo.maxBuyPeriodCount > 0 ||
cart.product.maxBuyCountInfo.maxBuyPersonCount > 0 ||
cart.product.maxBuyCountInfo.maxBuyTimeCount > 0
) {
return true;
}
};
/**
* get 카트 멀티 옵션 리스트 (=> {name, value}[])
*
* @param cart
* @return {{name: T, value: *}[]}
*/
const _getCartOptions = cart => {
const optionValues = cart.option.optionValue.split('|');
return cart.option.optionName
.split('|')
.map((optionName, index) => ({ name: optionName, value: optionValues[index] }));
};
/**
* get 카트 할인가
*
* @param cart
* @return {string}
* @private
*/
const _getCartDiscountHtml = cart => {
const discount =
cart.option.price.immediateDiscountAmt * cart.option.orderCnt + cart.option.price.additionalDiscountAmt;
if (discount > 0) {
return `-${shopby.utils.toCurrencyString(discount)}원`;
}
return '';
};
/**
* get 카트 적립금
* @param cart
* @return {string}
* @private
*/
const _getCartAccumulationHtml = cart => {
if (cart.option.accumulationAmtWhenBuyConfirm) {
cart.accumulationAmtHtml = `${shopby.utils.toCurrencyString(cart.option.accumulationAmtWhenBuyConfirm)}${
shopby.cache.getMall().accumulationConfig.accumulationUnit
}`;
}
return cart.accumulationAmtHtml;
};
/**
* get 카트 배송 정보
* @param cart
* @return {string}
* @private
*/
const _getCartDeliveryHtml = cart => {
let deliveryHtml = '-';
if (cart.deliveryRowSpan && cart.product.deliverable && cart.valid) {
if (cart.delivery.deliveryAmt > 0) {
deliveryHtml = `${shopby.utils.toCurrencyString(cart.delivery.deliveryAmt)}원`;
const isPayOnDelivery = cart.delivery.deliveryPayType === 'PAY_ON_DELIVERY';
deliveryHtml += isPayOnDelivery ? '(착불)' : '
(선결제)';
} else {
deliveryHtml = '무료';
}
}
return deliveryHtml;
};
/**
* 장바구니 데이터 컨버팅
* - price 정보는 그대로 리턴
* - 장바구니 리스트 데이터 컨버팅 (deliveryGroups + invalidProducts 조합 & 재배치)
* --- api 호출시 deliveryGroup - product - option 기준으로 3-depth 배열로 데이터가 내려옴 (+
* invalid 옵션들)
* --- 렌더링이 쉽도록 옵션 기준으로 1-depth 배열로 만들며 상품정보, 배송정보 등을 추가
* --- valid 가 false 인 옵션은 delivery, deliveryNo 정보가 없음
*
* - 자세한 내용은 GET /cart 문서 참조
*
* @author Daejoong Son
* @return list - [{deliveryNo, valid, delivery, product, option}]
* @return price - {...}
*/
const _convertCarts = data => {
let list = [];
if (data) {
const deliveryGroups = data.deliveryGroups || [];
const invalidProducts = data.invalidProducts || [];
let deliveryNo = -1;
list = deliveryGroups
.flatMap(delivery =>
delivery.orderProducts
.flatMap(product =>
product.orderProductOptions.map(option => ({
deliveryNo,
valid: true,
delivery,
product,
option,
})),
)
// 퓨어 데이터 부분과 렌더링 데이터 설정하는 부분을 분리하고 싶었는데, deliveryRowSpan 은 여기서 계산하기가 편해서...
.map((cart, index, carts) => {
cart.deliveryRowSpan = index === 0 ? carts.length : 0;
return cart;
}),
)
.concat(
invalidProducts.flatMap(product =>
product.orderProductOptions.map(option => ({
valid: false,
product,
option,
deliveryRowSpan: 1,
})),
),
)
.map((cart, index, carts) => {
// 구매수량 제한 추가
cart.isLimitations = _isLimitations(cart);
// 중복 여부 추가
cart.isDuplicateCart = _isDuplicateCart(cart, carts);
// 옵션 리스트 추가
cart.option.options = _getCartOptions(cart);
// 가격
cart.priceHtml = `${shopby.utils.toCurrencyString(cart.option.price.standardAmt)}원`;
// 할인가
cart.discountHtml = _getCartDiscountHtml(cart);
// 적립금
cart.accumulationAmtHtml = _getCartAccumulationHtml(cart);
// 최종 금액
cart.totalPriceHtml = `${shopby.utils.toCurrencyString(cart.option.price.buyAmt)}원`;
// 배송지 정보
cart.deliveryHtml = _getCartDeliveryHtml(cart);
return cart;
});
}
return list;
};
shopby.helper.cart = {
/**
* 장바구니 데이터 가져오기
* - GET /cart api 호출
* - 장바구니 리스트 데이터 컨버팅 (convertCartList 호출)
* - Promise 로 동작 - getCartData().then(cartData => {...})
*
* @author Daejoong Son
*/
async getCartData(isRefresh = false) {
if (cartData && !isRefresh) {
return cartData;
}
let data = null;
if (shopby.logined()) {
// 회원 장바구니
data = await shopby.api.order
.getCart({ queryString: { divideInvalidProducts: true } })
.then(response => response.data);
} else {
const guestCartList = shopby.localStorage.getItem(shopby.cache.key.cart.guestInfo) || [];
// 비회원 장바구니
if (guestCartList && guestCartList.length > 0) {
data = await shopby.api.order
.postGuestCart({
queryString: { divideInvalidProducts: true },
requestBody: guestCartList,
})
.then(response => response.data);
}
}
shopby.setGlobalVariableBy('CART', data);
// data 가 유효하면 converting
if (data) {
cartData = {
list: _convertCarts(data),
price: data.price,
};
} else {
cartData = {
list: [],
price: null,
};
}
console.log(cartData);
return cartData;
},
/**
* - 장바구니 추가
*
* TODO - 테스트 필요
* TODO - items 확인 필요 - 기존 코드에 createCartData 호출하는 부분 참조하삼
*
* @param carts
* @return {boolean}
*/
async addCart(cartPayloads) {
if (shopby.utils.isArrayEmpty(cartPayloads)) {
return false;
}
let isSuccess = false;
if (shopby.logined()) {
isSuccess = await shopby.api.order
.postCart({ requestBody: cartPayloads })
.then(() => true)
.catch(() => Promise.reject());
} else {
const {
storagePeriod,
cartEquivalentOptionUnitType,
storageMaxQuantity: guestCartStorageMaxQuantity,
} = shopby.cache.getMall().cartConfig;
const guestCartStoragePeriodByMilliseconds = shopby.utils.convertDaysToMilliseconds(storagePeriod);
const currentDate = new Date().getMilliseconds();
const newExpireDate = guestCartStoragePeriodByMilliseconds
? currentDate + guestCartStoragePeriodByMilliseconds
: null;
const prevCarts = shopby.localStorage.getItem(shopby.cache.key.cart.guestInfo) || [];
let newCartsBucket = [];
if (cartEquivalentOptionUnitType === 'QUANTITY') {
const cartPayloadIndicesWhichAreIntersected = [];
const updatedPrevCarts = prevCarts.map(({ optionNo, optionInputs, orderCnt, ...info }) => {
const cartPayloadIdxWhichHasSameOptionNo = cartPayloads.findIndex(
({ optionNo: payloadOptionNo, optionInputs: payloadOptionInputs }) =>
payloadOptionNo === optionNo &&
!(payloadOptionInputs && payloadOptionInputs.length) &&
!(optionInputs && optionInputs.length),
);
if (cartPayloadIdxWhichHasSameOptionNo !== -1)
cartPayloadIndicesWhichAreIntersected.push(cartPayloadIdxWhichHasSameOptionNo);
return {
optionNo,
optionInputs,
orderCnt:
Number(orderCnt) +
Number(
(cartPayloads[cartPayloadIdxWhichHasSameOptionNo] &&
cartPayloads[cartPayloadIdxWhichHasSameOptionNo].orderCnt) ||
0,
),
...info,
};
});
const notIntersectedCartPayload = cartPayloads.filter(
(_, idx) => !cartPayloadIndicesWhichAreIntersected.includes(idx),
);
newCartsBucket = [
...updatedPrevCarts,
...notIntersectedCartPayload.map(info => ({ ...info, cartNo: 0, expireDate: newExpireDate })),
];
} else {
newCartsBucket = [
...prevCarts,
...cartPayloads.map(info => ({ ...info, cartNo: 0, expireDate: newExpireDate })),
];
}
const sliceOffset =
guestCartStorageMaxQuantity && newCartsBucket.length > guestCartStorageMaxQuantity
? newCartsBucket.length - guestCartStorageMaxQuantity
: 0;
const newCarts = newCartsBucket.slice(sliceOffset).map((cart, idx) => ({
...cart,
cartNo: idx + 1, // 카트 번호 재정렬
}));
shopby.localStorage.setItem(shopby.cache.key.cart.guestInfo, newCarts);
isSuccess = true;
}
return isSuccess;
},
/**
* - 장바구니 추가
*
* TODO - 테스트 필요
*
* @param cartNos - [cartNo, ...]
* @return {boolean}
*/
async removeCarts(cartNos) {
if (shopby.utils.isArrayEmpty(cartNos)) {
return false;
}
let isSuccess = false;
if (shopby.logined()) {
isSuccess = await shopby.api.order
.deleteCart({ queryString: { cartNo: cartNos.join(',') } })
.then(() => true)
.catch(() => false);
} else {
let guestCartList = shopby.localStorage.getItem(shopby.cache.key.cart.guestInfo);
guestCartList = guestCartList.filter(cart => !cartNos.some(cartNo => cartNo === cart.cartNo));
shopby.localStorage.setItem(shopby.cache.key.cart.guestInfo, guestCartList);
isSuccess = true;
}
return isSuccess;
},
async getCartsCalculate(cartNos) {
if (shopby.utils.isArrayEmpty(cartNos)) {
return defaultPrice;
}
if (shopby.logined()) {
return await shopby.api.order
.getCartCalculate({ queryString: { cartNo: cartNos.join(',') } })
.then(response => response.data);
} else {
let guestCartList = shopby.localStorage.getItem(shopby.cache.key.cart.guestInfo) || [];
guestCartList = guestCartList.filter(cart => cartNos.some(cartNo => cartNo === cart.cartNo));
return await shopby.api.order
.postGuestCart({ requestBody: guestCartList })
.then(response => response.data.price);
}
},
/**
* cartCount API가 10배 빠름 이걸 사용..
*
* @author Haekyu Cho
*/
async getCartCount(isChangeCartCount = false) {
if (shopby.logined()) {
let count = shopby.localStorage.getItemWithExpire(shopby.cache.key.cart.count);
if (isChangeCartCount || !count) {
count = await shopby.api.order.getCartCount().then(({ data }) => data.count);
shopby.localStorage.setItemWithExpire(shopby.cache.key.cart.count, count);
return count;
}
if (Number.isInteger(count)) {
return count;
}
}
const guestInfo = shopby.localStorage.getItem(shopby.cache.key.cart.guestInfo);
const guestCartList = guestInfo ? guestInfo : [];
return guestCartList.length;
},
async updateCartCount(isChangeCartCount) {
await this.getCartCount(isChangeCartCount);
shopby.header.top.setData();
},
};
})();
(() => {
class Timer {
constructor(onTimerTicked, onTimerEnded, onTimerCleared) {
this.onTimerTicked = onTimerTicked;
this.onTimerEnded = onTimerEnded;
this.onTimerCleared = onTimerCleared;
this.count = 0;
this.timer = null;
this.isRunning = false;
}
startTimer() {
let minutes = null;
let seconds = null;
this.timer = setInterval(() => {
minutes = parseInt(this.count / 60, 10);
seconds = parseInt(this.count % 60, 10);
minutes = minutes < 10 ? '0' + minutes : minutes;
seconds = seconds < 10 ? '0' + seconds : seconds;
this.count -= 1;
// 타이머 끝
if (this.count < 1) {
clearInterval(this.timer);
this.onTimerEnded && this.onTimerEnded();
this.isRunning = false;
} else {
this.onTimerTicked && this.onTimerTicked(minutes + ':' + seconds);
}
}, 1000);
this.isRunning = true;
}
start(count) {
this.count = count;
if (!this.isRunning) {
this.startTimer();
} else {
clearInterval(this.timer);
this.startTimer();
}
}
clear() {
if (this.isRunning) {
clearInterval(this.timer);
this.onTimerCleared && this.onTimerCleared();
}
}
}
shopby.helper.timer = Timer;
})();
/*
* 옵션 헬퍼 (메인, 상품상세, 장바구니)
* @param optionInfo: {}, required - shopby.api.product.getProductsProductNoOptions 리턴된 값
* @param reservationData: {}, 상품의 reservationData
* @param selectedOptionNo: number, optional - 선택된 옵션(옵션 번호를 갖고 있는 최종) 초기값 세팅을 위한
* @param textOptions: { inputLabel: '', inputValue: '', required: boolean }[], optional - 텍스트옵션 초기값 세팅을 위한 값
* @return new Option()
* 옵션셀렉터, 텍스트 옵션, 선택된 옵션등의 데이터를 변경하기 위해서는 new Option() 에 있는 changeSomething 메소드를 사용해주세요.
*
* @author Bomee Yoon
* */
(() => {
shopby.helper.option = (optionInfo, reservationData, selectedOptionNo, textOptions) => {
const selectOption = new SelectOption(optionInfo, selectedOptionNo, reservationData);
const textOption = new TextOption(optionInfo, textOptions);
return new Option(selectOption, textOption, new SelectedOption(reservationData, selectOption));
};
class Option {
constructor(option, textOption, selectedOption) {
this.option = option;
this.textOption = textOption;
this.selectedOption = selectedOption;
this._addSelectedOption(true);
}
get options() {
// 옵션셀렉터를 위한 데이터
return this.option.options;
}
get textOptions() {
// 선택된 옵션의 텍스트옵션 데이터
return this.textOption.options;
}
get selectedOptions() {
// 최종 선택된 옵션 데이터
return this.selectedOption.options;
}
get totalPrice() {
// 최종 선택된 옵션가
return this.selectedOption.totalPrice;
}
/*
* @param isChangeType
* true : 선택된 옵션으로 변경되는 형식 (예: 장바구니) - selectedOptions 조회 시 마지막 옵션까지 선택하지 않으면 [] 을 반환
* false: 마지막 옵션까지 선택했을 때, 선택된 옵션이 추가되는 형식 (예: 상품상세) - selectedOptions 조회 시 마지막 옵션까지 선택하지 않으면 이전에 선택된 값들만 반환
* */
changeSelectOption(selectedDepth, selectedIndex, isChangeType = false) {
// 옵션셀렉터 변경 시 사용
this.option.changeSelectOption(selectedDepth, selectedIndex);
isChangeType ? this._changeSelectedOption() : this._addSelectedOption();
}
resetSelectOption() {
this.option.resetSelectOption();
}
changeTextOption(inputMatchingType, targetOptionNo, targetInputNo, value, amountIndex) {
// 선택된 옵션의 텍스트 옵션 변경 시 사용
this.textOption.changeTextOptionByProduct(inputMatchingType, targetInputNo, value);
this.selectedOption.changeTextOptionByOptionOrAmount(
inputMatchingType,
targetInputNo,
value,
targetOptionNo,
amountIndex,
);
}
changeOrderCount(type, optionNo, customOrderCount) {
// 선택된 옵션의 주문수량 변경 시 사용
this.selectedOption.changeOrderCount(type, optionNo, customOrderCount);
}
drawAmountTextOption(optionNo, textInputNo, orderCnt) {
// 선택된 옵션의 옵션별/수량별 텍스트 옵션
this.selectedOption.drawAmountTextOption(optionNo, textInputNo, orderCnt);
}
deleteSelectedOption(targetOptionNo) {
// 선택된 옵션 삭제 시 사용
this.selectedOption.deleteSelectedOption(targetOptionNo);
}
validate() {
validateOption(this.selectedOption.options, this.textOption.options);
}
_changeSelectedOption() {
const selectedOptionInfo = this.option.selectedOptionDetail;
const newTextOption = this.textOption.addNewTextOption(selectedOptionInfo);
this.selectedOption.changeSelectedOption(selectedOptionInfo, newTextOption, this.option.optionNo);
}
_addSelectedOption(isInit) {
if (!this.option.optionNo) return;
const selectedOptionInfo = this.option.selectedOptionDetail;
const newTextOption = this.textOption.addNewTextOption(selectedOptionInfo, isInit);
this.selectedOption.addSelectedOption(
selectedOptionInfo,
newTextOption,
this.option.optionNo,
this.option.selectedOptionsValues,
);
}
}
class SelectOption {
constructor(optionInfo, selectedOptionNo, reservationData) {
this._selectOptions = [];
this._optionNo = 0;
this._selectedOptionDetail = {};
this._selectedOptionsValues = [];
this._reservationData = reservationData;
const { selectType, multiLevelOptions, labels, flatOptions, displayableStock } = optionInfo;
this._originSelectOptions = selectType === 'MULTI' ? multiLevelOptions : flatOptions;
this._labels = selectType === 'MULTI' ? labels : [flatOptions[0].label];
this._lastDepth = selectType === 'MULTI' ? labels.length - 1 : 0;
this._selectType = selectType;
this._displayableStock = displayableStock;
this._convertOptionInfo(flatOptions, selectedOptionNo, selectType);
}
get options() {
return this._selectOptions;
}
get optionNo() {
return this._optionNo;
}
get selectedOptionDetail() {
return this._selectedOptionDetail;
}
get selectedOptionsValues() {
return this._selectedOptionsValues;
}
get isPreSalePeriod() {
return shopby.utils.isPreSalePeriod(this._reservationData);
}
changeSelectOption(selectedDepth, selectedIndex) {
if (selectedIndex === 0) {
throw new Error('옵션을 선택하세요.');
}
this._setSelectOptions(this._getSelectedValues(selectedDepth, selectedIndex));
this._setSelectedOptionDetail(selectedDepth, selectedIndex);
}
resetSelectOption() {
this._setSelectOptions(this._getSelectedValues(0, 0));
}
_getSelectedValues(selectedDepth, selectedIndex) {
const getSelectedMultiOption = depth => this._selectOptions[depth];
const mapSelectedValues = index => {
if (selectedDepth < index) {
return '';
}
if (selectedDepth === index) {
return getSelectedMultiOption(selectedDepth).values[selectedIndex];
}
const cumulativeSelectOption = getSelectedMultiOption(index);
const cumulativeSelectOptionIndex = cumulativeSelectOption.selectedIndex;
return cumulativeSelectOption.values[cumulativeSelectOptionIndex];
};
return this._selectOptions.map((_, index) => mapSelectedValues(index));
}
_setSelectOptions(values) {
this._selectOptions = [];
getSelectOptions(
this._originSelectOptions,
this._labels,
values,
0,
true,
this._selectOptions,
this._selectType,
this._reservationData,
this._displayableStock,
);
}
_getSelectedOptionDetail(selectedDepth, selectedIndex, selectedOptionNo) {
const automaticallySelectedLstDepthOption =
this._selectOptions[this._lastDepth].selectedIndex > 0 &&
this._selectOptions[this._lastDepth].details.length === 1;
if (selectedOptionNo > 0) {
return this._selectOptions[this._lastDepth].details.find(({ optionNo }) => optionNo === selectedOptionNo);
}
if (automaticallySelectedLstDepthOption && selectedIndex > 1) {
return this._selectOptions[this._lastDepth].details[0];
}
return this._selectOptions[this._lastDepth].details[selectedIndex - 1];
}
_isSelectedLastOption(selectedOptionNo) {
const { selectedIndex, details } = this._selectOptions[this._lastDepth];
const isSelectedLastDepthOption = selectedIndex > 0;
const automaticallySelectedLstDepthOption = isSelectedLastDepthOption && details.length === 1;
return selectedOptionNo > 0 || isSelectedLastDepthOption || automaticallySelectedLstDepthOption;
}
_setSelectedOptionDetail(selectedDepth, selectedIndex, selectedOptionNo) {
const selectedOptionValues = this._selectOptions.flatMap(options => {
if (options.selectedIndex > 0) {
return options.values[options.selectedIndex];
}
});
//마지막 옵션
if (this._isSelectedLastOption(selectedOptionNo)) {
const selectedOptionDetail = this._getSelectedOptionDetail(selectedDepth, selectedIndex, selectedOptionNo);
this._selectedOptionsValues = selectedOptionValues.join(' / ');
this._selectedOptionDetail = selectedOptionDetail;
this._optionNo = selectedOptionDetail.optionNo;
} else {
this._optionNo = 0;
}
}
_convertOptionInfo(flatOptions, selectedOptionNo = null, selectType = null) {
const selectedValues = getSelectedValues(flatOptions, selectedOptionNo, selectType);
this._setSelectOptions(selectedValues);
const needsInitialSelectedOptionDetail = flatOptions.length === 1 || selectedOptionNo > 0;
const optionNo = selectedOptionNo || (flatOptions.length > 0 && flatOptions[0].optionNo);
needsInitialSelectedOptionDetail && this._setSelectedOptionDetail(-1, -1, optionNo);
}
}
class TextOption {
constructor(optionInfo, initialTextOptions = []) {
this._textOptions = [...optionInfo.inputs];
this._setInitData(initialTextOptions);
}
get options() {
return this._textOptions;
}
addNewTextOption(selectedOption, isInit) {
const newTextOptions = [...this._textOptions].reduce((textOptions, textOption) => {
const { inputMatchingType, value } = textOption;
if (inputMatchingType === 'OPTION' || inputMatchingType === 'AMOUNT') {
textOptions.push({
...textOption,
value: isInit ? value : '',
amountValues: [],
});
}
return textOptions;
}, []);
return {
...selectedOption,
textOptions: newTextOptions,
};
}
changeTextOptionByProduct(inputMatchingType, targetInputNo, value) {
if (inputMatchingType !== 'PRODUCT') return;
this._textOptions = this._textOptions.map(option => {
const { inputMatchingType, inputNo } = option;
if (inputMatchingType === 'PRODUCT' && inputNo === targetInputNo) {
option.value = value;
}
return { ...option };
});
}
_setInitData(initialTextOptions) {
if (initialTextOptions.length === 0) return;
const found = inputLabel => this._textOptions.find(option => option.inputLabel === inputLabel);
this._textOptions = initialTextOptions.map(option => {
const initInputOption = found(option.inputLabel);
return {
inputNo: initInputOption.inputNo,
inputLabel: initInputOption.inputLabel,
inputMatchingType: initInputOption.inputMatchingType,
required: initInputOption.required,
value: option.inputValue,
};
});
}
}
class SelectedOption {
constructor(reservationData, option) {
this._selectedOptions = [];
this._reservationData = reservationData;
this.option = option;
}
get options() {
return this._selectedOptions;
}
get optionForCart() {
return this._selectedOptions;
}
get totalPrice() {
if (!this._selectedOptions.length) return 0;
return this._selectedOptions.reduce((totalPrice, { priceWithOptions }) => {
const currPrice = priceWithOptions.replace(/,/g, '');
totalPrice += parseInt(currPrice, 10);
return totalPrice;
}, 0);
}
addSelectedOption(selectedOptionInfo, newTextOption, newOptionNo, selectedOptionsValues) {
const HAS_ONLY_ONE_OPTION_CODE_NUMBER = -1;
if (!selectedOptionInfo && newOptionNo === HAS_ONLY_ONE_OPTION_CODE_NUMBER) return;
if (this._selectedOptions.some(({ optionNo }) => optionNo === selectedOptionInfo.optionNo)) {
shopby.alert('이미 선택된 옵션입니다.');
this.option.resetSelectOption();
} else {
this._selectedOptions.push(
this._createNewSelectedOption(selectedOptionInfo, newTextOption, selectedOptionsValues),
);
}
}
changeSelectedOption(selectedOptionInfo, newTextOption, newOptionNo) {
this._selectedOptions = newOptionNo > 0 ? [this._createNewSelectedOption(selectedOptionInfo, newTextOption)] : [];
}
changeOrderCount(action, optionNo, customOrderCount) {
const isPreSalePeriod = shopby.utils.isPreSalePeriod(this._reservationData);
const selectedOption = this._selectedOptions.find(({ optionNo: no }) => no === optionNo);
const { orderCnt, stockCnt, reservationStockCnt } = selectedOption;
const orderStockCnt = isPreSalePeriod ? reservationStockCnt : stockCnt;
const { displayableStock } = this.option;
switch (action) {
case 'up':
if (!displayableStock && orderCnt <= 99999999) {
// 수량 미노출일 경우 99999999까지 입력 가능
selectedOption.orderCnt += 1;
break;
}
if (orderCnt < orderStockCnt) {
selectedOption.orderCnt += 1;
}
break;
case 'down':
if (orderCnt > 1) {
selectedOption.orderCnt -= 1;
}
break;
default:
if (customOrderCount <= 1) {
selectedOption.orderCnt = 1;
break;
}
if (displayableStock && customOrderCount > stockCnt) {
selectedOption.orderCnt = stockCnt;
break;
}
if (!displayableStock && customOrderCount > 99999999) {
// 수량 미노출일 경우 99999999를 초과하여 입력할 수 없다.
selectedOption.orderCnt = 99999999;
break;
}
selectedOption.orderCnt = customOrderCount;
break;
}
selectedOption.priceWithOptions = this._getPriceWithOption(selectedOption);
}
//옵션 수량이 2개 이상일때 옵션별 텍스트옵션 <=> 수량별 텍스트옵션 입력가능
drawAmountTextOption(optionNo, textInputNo, orderCnt) {
const selectedOption = this._selectedOptions.find(({ optionNo: no }) => no === optionNo);
const { textOptions } = selectedOption;
textOptions.forEach(textOption => {
if (textOption.inputNo !== textInputNo) return;
if (orderCnt > 1 && !textOption.isRealAmountMode) {
textOption.isRealAmountMode = true;
//옵션별 수량과 텍스트 옵션 개수가 같을 때
if (textOption.amountValues.length === orderCnt - 1) return;
//옵션별 수량과 텍스트 옵션 수량이 다를 경우 초기화, 예. 옵션별 수량 변경된 경우
textOption.amountValues = [];
textOption.amountValues = Array(orderCnt - 1)
.fill(null)
// eslint-disable-next-line no-unused-vars
.map(_ => ({ value: '' }));
} else if (textOption.isRealAmountMode) {
textOption.isRealAmountMode = false;
}
});
}
changeTextOptionByOptionOrAmount(inputMatchingType, targetInputNo, value, targetOptionNo, amountIndex) {
if (targetOptionNo > 0) {
const selectedTextOptions = this._selectedOptions
.filter(({ optionNo }) => optionNo === targetOptionNo)
.flatMap(({ textOptions }) => textOptions);
switch (inputMatchingType) {
case 'OPTION':
selectedTextOptions.forEach(textOption => {
if (textOption.inputNo === targetInputNo) {
textOption.value = value;
}
});
break;
case 'AMOUNT':
selectedTextOptions.forEach(textOption => {
if (textOption.inputNo === targetInputNo) {
textOption.amountValues[amountIndex].value = value;
}
});
break;
default:
throw new Error('INVALID_TEXT_OPTION_MATCHING_TYPE');
}
}
}
deleteSelectedOption(targetOptionNo) {
this._selectedOptions = this._selectedOptions.filter(({ optionNo }) => optionNo !== targetOptionNo);
}
_getPriceWithOption(selectedOption, orderCnt) {
orderCnt = orderCnt >= 0 ? orderCnt : selectedOption.orderCnt;
const priceWithOption = selectedOption.buyPrice * orderCnt;
return shopby.utils.toCurrencyString(priceWithOption);
}
_createNewSelectedOption(selectedOption, newTextOption, selectedOptionsValues) {
const orderCnt = 1;
const priceWithOptions = this._getPriceWithOption(selectedOption, orderCnt);
return {
...newTextOption,
orderCnt,
priceWithOptions,
selectedOptionsValues,
};
}
}
const getStockLabel = (option, reservationData, displayStock) => {
const { saleType, forcedSoldOut, stockCnt, reservationStockCnt } = option;
//옵션 : 판매기간동안 판매재고 0 , 예약판매기간동안 예약재고 0
if (saleType === 'SOLDOUT') {
return {
disabled: 'disabled',
stockStatusLabel: '[품절]',
};
}
if (forcedSoldOut) {
return {
disabled: 'disabled',
stockStatusLabel: '[임시품절]',
};
}
if (displayStock) {
//예약기간동안 예약재고 노출
if (shopby.utils.isPreSalePeriod(reservationData)) {
return {
disabled: '',
stockStatusLabel: `재고 : ${reservationStockCnt}개`,
};
}
return {
disabled: '',
stockStatusLabel: `재고 : ${stockCnt}개`,
};
} else {
// 재고 미 노출
return {
disabled: '',
stockStatusLabel: ``,
};
}
};
const getAddPriceLabel = option => {
const { addPrice } = option;
if (!addPrice) return '';
return `(${addPrice > 0 ? '+' : ''}${shopby.utils.toCurrencyString(addPrice)} 원)`;
};
const getSelectedValues = (flatOptions, selectedOptionNo, selectType) => {
if (selectedOptionNo === null) return [];
const separator = '|';
return flatOptions
.filter(({ optionNo }) => optionNo === selectedOptionNo)
.flatMap(({ value }) => (value.includes(separator) && selectType === 'MULTI' ? value.split(separator) : [value]));
};
const getSelectOptionMessage = (isSelectedPrevDepthOption, labels, depth) => {
return isSelectedPrevDepthOption
? `${labels[depth]} 을(를) 선택해주세요.`
: `${labels[depth - 1]} 을(를) 먼저 선택해주세요.`;
};
const getLabels = (option, reservationData, displayableStock) => {
option.stockStatus = getStockLabel(option, reservationData, displayableStock);
option.priceLabel = getAddPriceLabel(option);
return option;
};
//마지막 옵션이 품절 또는 일시품절인 경우
const isLastOptionSoldOut = option => {
//마지막 뎁스 옵션인 경우
if (!option.children) return option.forcedSoldOut || option.saleType === 'SOLDOUT';
return option.children.every(child => isLastOptionSoldOut(child));
};
const createNextInitialSelectOption = (depth, label) => ({
depth,
label,
values: [],
optionValues: [],
details: [],
selectedIndex: 0,
});
const createNextSelectOptionInfo = (
currentDepthData,
isSelectedPrevDepthOption,
depth,
labels,
selectedValues,
selectType,
reservationData,
displayableStock,
) => {
const lastOptionAllSoldOutStatus = currentDepthData.flatMap(data => isLastOptionSoldOut(data));
const needsLastDepthCustomLabels = isSelectedPrevDepthOption && labels.length - 1 === depth;
const hasOnlyOneOption = currentDepthData.length === 1;
const nextDepth = depth + 1;
const nextSelectOption = createNextInitialSelectOption(depth, labels[depth]);
nextSelectOption.values.push(getSelectOptionMessage(isSelectedPrevDepthOption, labels, depth));
nextSelectOption.optionValues.push({
disabled: 'disabled',
value: getSelectOptionMessage(isSelectedPrevDepthOption, labels, depth),
});
let nextDepthData = [];
let isSelectedCurrentOption = false;
currentDepthData.forEach(({ value, children, ...rest }, index) => {
let label;
if (needsLastDepthCustomLabels || selectType === 'FLAT') {
const { priceLabel, stockStatus } = { ...getLabels({ ...rest }, reservationData, displayableStock) };
//label은 재고, 옵션가처럼 상품 뒤에 따라오는 부연 텍스트들을 모아놓은 변수입니다.
//옵션명과 겹치지 않고 언제든지 커스텀 할 수 있게 따로 전역변수로 설정을 했습니다.
//만약 '옵션가, 재고'가 옵션에 포함이 안되더라도 빈값으로('') 반환됩니다.
label = `${priceLabel}${stockStatus.stockStatusLabel ? ' / ' + stockStatus.stockStatusLabel : ''}`;
const customValue = `${value} ${label}`;
const detail = {
...rest,
disabled: stockStatus.disabled,
value: `${value} | ${priceLabel}`,
};
nextSelectOption.values.push(customValue);
nextSelectOption.details.push(detail);
nextSelectOption.optionValues.push({
...detail,
lastValue: `${value} / ${priceLabel}`,
value: customValue,
});
} else {
isSelectedPrevDepthOption && nextSelectOption.values.push(value);
isSelectedPrevDepthOption &&
nextSelectOption.optionValues.push({
...rest,
disabled: lastOptionAllSoldOutStatus[index] ? 'disabled' : '',
value: lastOptionAllSoldOutStatus[index] ? `${value} | [품절]` : value,
});
}
const { forcedSoldOut, saleType } = { ...rest };
const isSoldOut = forcedSoldOut || saleType === 'SOLDOUT';
const selectedOptionValues = ((value, selectedValue) => {
//selectedValue는 내가 선택한 옵션 텍스트이고
//value는 실제 데이터 값(string)입니다
//처음 랜더링 시에는 selectedValue가 undefined로 할당이 되서 분기문으로 처리를 했습니다.
if (selectedValue) {
return selectedValue.replace(label, '').trim() === value.trim();
}
})(value, selectedValues[depth]);
const isSelectedCurrentValue = selectedOptionValues && isSelectedPrevDepthOption;
if ((!isSoldOut && isSelectedCurrentValue) || hasOnlyOneOption) {
nextSelectOption.selectedIndex = index + 1;
isSelectedCurrentOption = true;
nextDepthData = children;
}
});
return {
nextDepthData,
nextSelectOption,
nextDepth,
isSelectedCurrentOption,
};
};
const getSelectOptions = (
currentDepthData,
labels,
selectedValues,
depth,
isSelectedPrevDepthOption,
nextSelectOptions,
selectType,
reservationData,
displayableStock,
) => {
const isLastDepth = labels.length === depth;
if (isLastDepth) return;
const { nextDepthData, nextSelectOption, nextDepth, isSelectedCurrentOption } = createNextSelectOptionInfo(
currentDepthData,
isSelectedPrevDepthOption,
depth,
labels,
selectedValues,
selectType,
reservationData,
displayableStock,
);
nextSelectOptions.push(nextSelectOption);
if (selectType === 'MULTI') {
getSelectOptions(
nextDepthData,
labels,
selectedValues,
nextDepth,
isSelectedCurrentOption,
nextSelectOptions,
selectType,
reservationData,
displayableStock,
);
}
};
const validateOption = (selectedOptions, textOptions) => {
selectedOptions = selectedOptions || [];
if (!selectedOptions.length) {
throw new Error('옵션을 선택해주세요.');
}
const isEmptyRequiredTextOptionByProduct = textOptions
.filter(({ inputMatchingType }) => inputMatchingType === 'PRODUCT')
.filter(({ required }) => required)
.some(({ value }) => !value);
if (isEmptyRequiredTextOptionByProduct) {
throw new Error('상품별 텍스트옵션(필수)을 입력해주세요.');
}
const isEmptyRequiredTextOptionByOption = selectedOptions
.flatMap(({ textOptions }) => textOptions)
.some(({ required, value }) => required && !value);
if (isEmptyRequiredTextOptionByOption) {
throw new Error('옵션별 텍스트옵션(필수)을 입력해주세요.');
}
};
/*
* 장바구니 담기 request body
* */
shopby.helper.option.requestBodyForCartOrOrder = (productNo, selectedOptions, textOptions, optionUsed = true) => {
const textOptionsByProduct = convertTextOptionsBy(textOptions, 'PRODUCT');
let selectedOptionsByAmountTextOptions = []; //수량별 텍스트 옵션이 있는 경우 그 개수만큼 담은 selectedOptions
const optionNosWithAmountTextOptions = []; //selectedOptionsByAmountTextOptions에 담긴 옵션 번호들
let beforeAmountTextOptionCount = selectedOptionsByAmountTextOptions.length; //이전에 담았던 수량별텍스트옵션개수 , textOptionsCombinationByAmount에 담기 위해 필요한 계산 변수
//수량별텍스트옵션확인
// selectedOptions 기존 옵션정보에서 수량별 텍스트 옵션있는지 확인하고 있으면 selectedOptionsByAmountTextOptions에 담기
const checkAmountTextOptions = option => {
beforeAmountTextOptionCount = selectedOptionsByAmountTextOptions.length;
const textOptionByAmount = option.textOptions.find(textOption => textOption.isRealAmountMode);
const textOptionCountByAmount = textOptionByAmount && textOptionByAmount.amountValues.length;
if (!textOptionByAmount || textOptionCountByAmount <= 0) return;
addSelectedOptionsByAmountTextOptions(option, textOptionCountByAmount);
};
//수량별 텍스트 옵션이 있는 옵션 담기
const addSelectedOptionsByAmountTextOptions = (option, textOptionCountByAmount) => {
selectedOptionsByAmountTextOptions = selectedOptionsByAmountTextOptions.concat(
Array.from({ length: textOptionCountByAmount + 1 }, () => ({
...option,
})),
);
optionNosWithAmountTextOptions.push(option.optionNo);
};
const getOptionInputs = textOptions => {
return textOptionsByProduct.concat(convertTextOptionsBy(textOptions));
};
const getTextOptionsCombinationByAmount = (textOptionsByAmount, optionIndex) => {
const textOptionNosByAmount = textOptionsByAmount.map(option => option.inputNo);
const textOptionNoCountByAmount = [...new Set(textOptionNosByAmount)].length;
const textOptionCombinationIndexByAmount =
optionIndex - beforeAmountTextOptionCount < 0 ? optionIndex : optionIndex - beforeAmountTextOptionCount;
const textOptionCountByAmountPerOptionNo = textOptionsByAmount.length / textOptionNoCountByAmount;
//수량별 텍스트옵션이 여러개인 경우
return Array.from(
{ length: textOptionNoCountByAmount },
(_, i) => textOptionsByAmount[textOptionCountByAmountPerOptionNo * i + textOptionCombinationIndexByAmount],
);
};
//수량별 텍스트옵션이 없는 경우 : optionInputs에 상품별 텍스트옵션, 옵션별 텍스트옵션만 담는다.
const selectedOptionsWithoutAmountTextOptions = selectedOptions
.map(option => {
const { optionNo, orderCnt, textOptions } = option;
checkAmountTextOptions(option);
const textOptionsByOption = getOptionInputs(textOptions).filter(
textOption => !textOption.isRealAmountMode && textOption.inputMatchingType === 'OPTION',
);
return {
productNo,
optionNo,
orderCnt,
channelType: null,
optionInputs: [...textOptionsByProduct, ...textOptionsByOption],
additionalProductNo: 0,
optionUsed,
};
})
.filter(({ optionNo }) => !optionNosWithAmountTextOptions.includes(optionNo));
//수량별 텍스트 옵션이 있는 경우 : optionInputs에 상품별 텍스트옵션, 옵션별 텍스트옵션, 수량별 텍스트옵션(각각 1개씩) 담는다.
selectedOptionsByAmountTextOptions =
selectedOptionsByAmountTextOptions &&
selectedOptionsByAmountTextOptions.map((option, index) => {
const { optionNo, textOptions } = option;
const textOptionsByAmount = getOptionInputs(textOptions).filter(textOption => textOption.isRealAmountMode);
const textOptionsByOption = getOptionInputs(textOptions).filter(
textOption => !textOption.isRealAmountMode && textOption.inputMatchingType === 'OPTION',
);
const textOptionsCombinationByAmount = getTextOptionsCombinationByAmount(textOptionsByAmount, index);
return {
productNo,
optionNo,
orderCnt: 1, // 수량별이니까 하나씩
channelType: null,
optionInputs: [...textOptionsByProduct, ...textOptionsByOption, ...textOptionsCombinationByAmount],
additionalProductNo: 0,
optionUsed,
};
});
return [...selectedOptionsWithoutAmountTextOptions, ...selectedOptionsByAmountTextOptions];
};
const convertTextOptionsBy = (textOptions, type) =>
textOptions.reduce((acc, curr) => {
const { value: inputValue, inputMatchingType, amountValues } = curr;
if (!type || (type && inputMatchingType === type)) {
acc.push({ ...curr, inputValue });
}
if (amountValues && amountValues.length > 0) {
amountValues.forEach(amountValue => {
acc.push({ ...curr, inputValue: amountValue.value });
});
}
return acc;
}, []);
})();
/**
* 회원 관련 중복 로직 담아두는 헬퍼
*
* @author sohyun choi
*/
(() => {
shopby.helper.member = {
get termsTitle() {
return {
USE: [`${shopby.cache.getMall().mall.mallName} 이용약관`, true],
PI_COLLECTION_AND_USE_REQUIRED: ['회원가입시 개인정보 수집/이용 약관', true],
PI_COLLECTION_AND_USE_OPTIONAL: ['회원가입시 개인정보 수집/이용 약관', false],
PI_PROCESS_CONSIGNMENT: ['개인정보 처리/위탁 약관', false],
PI_THIRD_PARTY_PROVISION: ['개인정보 제3자 제공 약관', false],
PI_SELLER_PROVISION: ['개인정보 판매자 제공', true],
MARKETING_INFO_USAGE: ['마케팅 목적의 개인정보 수집/이용 동의', false],
MARKETING_RECEIVE: ['광고성 수신 동의', false],
};
},
async getTerms(termsTitle) {
const termsTypes = Object.keys(termsTitle).join(',');
const { data: terms } = await shopby.api.manage.getTerms({ queryString: { termsTypes } });
return terms;
},
async getAgreements(terms = null, termsTitle = null, checkedAgreements = null) {
termsTitle = termsTitle ? termsTitle : this.termsTitle;
terms = terms ? terms : await this.getTerms(termsTitle);
checkedAgreements = checkedAgreements ? checkedAgreements : [];
return Object.keys(terms)
.filter(termKey => terms[termKey].used)
.map(termKey => {
const key = termKey.toUpperCase();
const [label, required] = termsTitle[key];
return { ...terms[termKey], label, required, key, checked: checkedAgreements.includes(key) };
});
},
openKcpCallback(event) {
event.preventDefault();
const WIDTH = 410;
const HEIGHT = 500;
const leftpos = screen.width / 2 - WIDTH / 2;
const toppos = screen.height / 2 - HEIGHT / 2;
const popupOption = `width=${WIDTH},height=${HEIGHT},left=${leftpos},top=${toppos},toolbar=no,status=no,statusbar=no,menubar=no,scrollbars=no,resizable=no`;
window.open(location.origin + '/callback/kcp-auth-callback.html', 'auth_popup', popupOption);
},
get emailValue() {
const email = $('input[name="email"]')
.get()
.map(target => $(target).val());
return email.join('@');
},
getCurrentAuth(authType) {
const authTypeMap = {
email: { message: '이메일', value: this.emailValue, key: 'email', errorMessage: '이메일을 입력해주세요' },
sms: {
message: '휴대폰번호',
value: $('input[name="mobileNo"]').val(),
key: 'mobileNo',
errorMessage: '휴대폰번호를 입력해주세요',
},
};
return authTypeMap[authType];
},
changeSelectedBirth() {
const selectedYear = $('select[name="birthYear"]').val();
const selectedMonth = $('select[name="birthMonth"]').val();
const selectedDay = $('select[name="birthDay"]').val();
const $birthWarning = $('.member_birthday').find('.warning_message');
const existSelected = selectedYear !== '0' || selectedMonth !== '0' || selectedDay !== '0';
const notSelected = selectedYear === '0' || selectedMonth === '0' || selectedDay === '0';
if (existSelected && notSelected) {
$birthWarning.text(shopby.message.requiredBirthday).show();
return false;
}
const selectedBirthday = dayjs(new Date(selectedYear, selectedMonth, selectedDay));
const today = dayjs();
if (today.diff(selectedBirthday, 'year', true) < 14) {
$birthWarning.text(shopby.message.invalidAge).show();
return false;
}
$birthWarning.hide();
return true;
},
checkValidInput(inputWaringMessage) {
for (let i = 0; i < inputWaringMessage.length; i = i + 1) {
const usable = $(inputWaringMessage[i]).data('usable');
const isValidInput = usable !== false;
if (!isValidInput) return false;
}
return true;
},
submitValidation() {
const $requires = $('input[class="require"]');
const noRequireInput = $requires.toArray().some(el => $(el).val().length < 1 && $(el).is(':visible'));
if (noRequireInput) {
throw new Error('필수 입력 사항을 확인 바랍니다.');
}
const emailSend = !$('input[name="email"]').prop('disabled') && $('.btn_send_email').length > 0;
if (emailSend) {
throw new Error('이메일 인증을 진행해주세요.');
}
const mobileNo = $('input[name="mobileNo"]');
const smsSend = $('.btn_send_sms').length > 0 && !mobileNo.prop('disabled');
const certify = $('.btn_certification').length > 0 && !mobileNo.prop('disabled');
if (smsSend || certify) {
throw new Error('휴대폰 번호인증을 진행해주세요.');
}
const noAllAddressInput =
$('input[name="address"]').val().length > 0 && $('input[name="detailAddress"]').val().trim().length === 0;
if (noAllAddressInput) {
throw new Error('상세주소를 입력해주세요.');
}
if (!this.changeSelectedBirth()) {
throw new Error(shopby.message.invalidBirthday);
}
const marketingInfoUsageAgreed = document.querySelector('#marketingInfoUsageAgreed');
const marketingReceiveAgreed = document.querySelector('#directMailAgreed');
const smsAgreed = document.querySelector('#smsAgreed');
if (!marketingInfoUsageAgreed?.checked && (marketingReceiveAgreed?.checked || smsAgreed?.checked)) {
throw new Error('이메일 및 SMS 수신을 위해 마케팅 목적의 개인정보 수집·이용에 동의해 주세요.');
}
},
/**
* @return {'withDrawn'|'existId'|'invalidId'|'successId'}
*/
async userIdValidation(userId) {
if (userId.length < 5) {
return 'lessLengthId';
}
const { data: memberIdStatus } = await shopby.api.member.getProfileIdExists({
queryString: { memberId: userId },
});
if (memberIdStatus.exist) {
return memberIdStatus.status === 'WITHDRAWN' ? 'withDrawn' : 'existId';
}
const atCount = (userId.match(shopby.regex.at) || []).length;
if (shopby.regex.userid.test(userId) || atCount > 1) {
return 'invalidId';
}
return 'successId';
},
/**
* @return {'invalidPasswordSpecial'|'invalidPassword'|'lessInvalidPassword'|''}
*/
passwordValidation(password) {
if (password.length < 8 || password.length > 20) {
return 'invalidPassword';
}
const patternNumber = shopby.regex.number;
const patternEnglish = shopby.regex.eng;
const patternSpecial = shopby.regex.passwordSpecial;
const checkNumber = patternNumber.test(password) ? 1 : 0;
const checkEnglish = patternEnglish.test(password) ? 1 : 0;
const checkSpecial = patternSpecial.test(password) ? 1 : 0;
const checkSum = checkNumber + checkEnglish + checkSpecial;
const filteringPassword = password
.replace(patternNumber, '')
.replace(patternEnglish, '')
.replace(patternSpecial, '');
if (filteringPassword.length > 0) {
return 'invalidPasswordSpecial';
}
if (password.length < 10 && checkSum < 3) {
return 'invalidPassword';
} else if (checkSum < 2) {
return 'lessInvalidPassword';
}
return '';
},
/**
* @return {'notEqualPassword'|''}
*/
passwordChkValidation(passwordChk) {
return passwordChk && passwordChk === $('input[name="password"]').val() ? '' : 'notEqualPassword';
},
/**
* @return {'invalidKorEngNum'|''}
*/
nameValidation(name) {
if (name.length < 1) return '';
return shopby.regex.noSpecialSpace.test(name) ? 'invalidKorEngNum' : '';
},
/**
* @return {'existNickname'|'successNickname' |'invalidKorEngNum'|''}
*/
async nicknameValidation(nickname, originNickname = '') {
if (nickname.length < 1) return '';
if (originNickname && originNickname === nickname) return 'successNickname';
const { data: checkNickname } = await shopby.api.member.getProfileNicknameExists({
queryString: { nickname },
});
if (checkNickname.exist) {
return 'existNickname';
}
return shopby.regex.noSpecialSpace.test(nickname) ? 'invalidKorEngNum' : 'successNickname';
},
/**
* @return {'noInputEmail'|'invalidEmail'|'existEmail'|'successEmail'}
*/
async emailValidation(originEmail = '') {
if (originEmail && originEmail === this.emailValue) return 'successEmail';
const [id, domain] = this.emailValue.split('@');
if (!id || !domain) {
return 'noInputEmail';
}
if (!shopby.regex.emailId.test(id) || !shopby.regex.emailDomain.test(domain)) {
return 'invalidEmail';
}
const { data: checkEmail } = await shopby.api.member.getProfileEmailExists({
queryString: { email: this.emailValue },
});
return checkEmail.exist ? 'existEmail' : 'successEmail';
},
/**
* @return {'invalidMobileNo'|''}
*/
mobileNoValidation(mobileNo) {
return shopby.regex.mobileNo.test(mobileNo) ? '' : 'invalidMobileNo';
},
/**
* 120년 전까지의 년도 리스트
*
* @returns [{value: number, name: string}]
*/
get last120YearsAgo() {
const year = new Date().getFullYear();
return [...Array(120).keys()].reduce(
(yearList, index) => {
return [...yearList, { value: year - index, name: `${year - index}년` }];
},
[{ value: 0, name: '년도' }],
);
},
/**
* @returns [{value: number, name: string}]
*/
get monthList() {
return [...Array(13).keys()].map(index => ({ value: index, name: `${index === 0 ? '' : index}월` }));
},
/**
* @returns [{value: number, name: string}]
*/
get dayList() {
return [...Array(32).keys()].map(index => ({ value: index, name: `${index === 0 ? '' : index}일` }));
},
};
})();
/**
* 로그인 관련 중복 로직 담아두는 헬퍼
*
* @author sohyun choi
*/
(() => {
shopby.helper.login = {
goNextUrl() {
const nextUrl = shopby.utils.getUrlParam('next-url');
const referrer = document.referrer !== '';
const isJoinReferrer = document.referrer.includes('join');
// 다른 페이지에서 nextUrl 을 직접적으로 지정했을 경우 이 조건에 걸린다.
if (nextUrl) {
// 1. URL 형식일 경우 host 가 일치하는지 검사.
// 아닐 경우 URL 파싱이 실패하게 되고 catch 조건으로 이동
try {
const url = new URL(nextUrl);
// 호스트가 일치할 경우 리다이렉트
if (url.host === location.host) {
location.href = url.href;
// 일치하지 않을 경우 Home 으로 이동
} else {
shopby.goHome();
}
} catch (error) {
// 2. URL 형식이 아닐경우 pathname 형식일 수 있다.
// nextUrl 을 pathName 으로 가정하고 리다이렉트 시도. 실패할 경우 Home 으로 이동
try {
location.href = location.origin + nextUrl;
} catch (error) {
shopby.goHome();
}
}
} else if (referrer && !isJoinReferrer) {
if (document.referrer.includes('dormant.html')) {
shopby.goHome();
return;
}
window.location.href = document.referrer;
} else {
shopby.goHome();
}
},
popup: null,
async openIdLogin(event) {
event.preventDefault();
const provider = `ncp_${event.currentTarget.dataset.provider}`;
if (this.popup && !this.popup.closed) {
this.popup.close();
this.popup = null;
}
this.popup = window.open('about:blank', '간편 로그인', 'width=420px,height=550px,scrollbars=yes');
this.popup.focus();
const data = await this.fetchOauthLogin(provider);
this.openLoginPopup(data, provider);
},
async fetchOauthLogin(provider) {
const oauthToken = this.generateRandomString();
const redirectUri = shopby.utils.getRedirectUrl('/callback/auth-callback.html');
shopby.localStorage.setItemWithExpire(shopby.cache.key.member.oauthProvider, provider, 30 * 60 * 1000);
shopby.localStorage.setItemWithExpire(shopby.cache.key.member.oauthToken, oauthToken, 30 * 60 * 1000);
const { data } = await shopby.api.auth.getOauthLoginUrl({
queryString: {
provider,
redirectUri,
state: oauthToken,
},
});
return data;
},
/**
* n자리 랜덤 문자열 생성기
*
* @param maxLength - default: 6자리
* @returns {string}
*/
generateRandomString(maxLength = 6) {
const availableChar = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from(Array(maxLength).keys()).reduce(prev => {
return prev + availableChar.charAt(Math.floor(Math.random() * availableChar.length));
}, '');
},
openLoginPopup(result, provider, customCallback, popup = this.popup) {
window.shopOauthCallback = customCallback || this._openIdAuthCallback.bind(this);
popup.location.href = result.loginUrl;
},
_openIdAuthCallback(profileResult = null, ordinaryMemberData = null, isDormant = false) {
window.shopOauthCallback = null;
if (isDormant === true) {
shopby.confirm({ message: '휴면해제가 필요합니다.' }, function (e) {
if (e.state === 'ok') {
shopby.localStorage.setItem(shopby.cache.key.member.dormant, true);
location.replace('/pages/login/dormant.html');
}
});
return;
}
if (!profileResult) {
shopby.cache.removeAccessToken();
shopby.alert({ message: '간편 인증에 실패하였습니다.' });
return;
}
if (profileResult.memberStatus === 'WAITING' && !!ordinaryMemberData) {
shopby.popup('join-open-id-kakaosync', { ordinary: ordinaryMemberData }, this._JoinOpenIdKakaoSync);
} else if (profileResult.memberStatus === 'WAITING') {
shopby.popup('join-open-id', { profile: profileResult }, this._joinOpenId);
} else {
shopby.alert('로그인이 완료 되었습니다.', shopby.helper.login.goNextUrl);
}
},
async _JoinOpenIdKakaoSync(data) {
if (data.state === 'cancel') {
shopby.cache.removeAccessToken();
return;
} else if (data.state === 'login') {
shopby.helper.login.goNextUrl();
return;
}
try {
await shopby.api.member.postProfileOpenId({
requestBody: {},
});
shopby.alert({ message: '로그인이 완료 되었습니다.' }, shopby.helper.login.goNextUrl);
} catch (e) {
shopby.goHome();
}
},
async _joinOpenId({ state, agreedTerms: joinTermsAgreements }) {
if (state !== 'ok') {
shopby.cache.removeAccessToken();
return;
}
try {
const request = { requestBody: { joinTermsAgreements } };
await shopby.api.member.postProfileOpenId(request);
shopby.alert({ message: '회원가입이 완료 되었습니다.' }, shopby.helper.login.goNextUrl);
} catch (e) {
shopby.goHome();
}
},
};
})();
/**
* 첨부파일 헬퍼
*
* @author eunbi kim
*/
(() => {
shopby.helper.attachments = {
async onChangeAttachments({ target }, uploadImageCallback, currentAttachmentLength, maxLength = 10) {
const addedImagesCount = target.files.length;
if (currentAttachmentLength + addedImagesCount > maxLength) {
shopby.alert(`첨부파일은 최대 ${maxLength}개만 등록 가능합니다.`);
return;
}
const files = Array.from(target.files);
await Promise.all(
files.map(file => {
if (!this.validateFileExtension(file)) return;
if (!this.validateFileSize(file)) return;
return this.uploadImage(file, uploadImageCallback);
}),
);
},
validateFileExtension({ name }) {
if (name.match(shopby.regex.imageExtension)) return true;
shopby.alert('허용하지 않는 확장자입니다.');
return false;
},
validateFileSize({ size }) {
const maxSize = 5 * 1024 * 1024;
if (Number(size) <= maxSize) return true;
shopby.alert('파일용량이 5MB를 초과하였습니다.');
return false;
},
async uploadImage(file, uploadImageCallback) {
try {
const formData = new FormData();
await formData.append('file', file);
const { data } = await shopby.api.storage.postFilesImages({ requestBody: formData });
uploadImageCallback(data, file);
} catch (e) {
console.error(e);
}
},
};
})();
/**
* 디자인 팝업
*
* @author Daejoong Son
*/
(() => {
let designPopups = [];
const today = shopby.date.today();
const invisibleToday = shopby.localStorage.getItem(shopby.cache.key.designPopups.invisibleToday) || {
day: today,
list: [],
};
if (today !== invisibleToday.day) {
invisibleToday.day = today;
invisibleToday.list = [];
}
/**
* url 기반으로 pageType 과 targetNo 정보 만들기
*
* @return {null|{pageType: string, targetNo: string}|{pageType: string}|{pageType: string, target: string}}
*/
const getPageTypeInformation = ({ pathnameWithoutPlatformType }) => {
const { searchParams } = new URL(window.location.href);
const pathnameInCategoryPage = shopby.utils.getPathNameWithoutPlatform('/pages/product/list.html');
if (pathnameWithoutPlatformType.includes(pathnameInCategoryPage)) {
const categoryNo = searchParams.get('categoryNo');
if (categoryNo) {
return {
pageType: 'CATEGORY',
searchKey: 'categoryNo',
targetNo: categoryNo,
};
}
}
const pathnameInProductPage = shopby.utils.getPathNameWithoutPlatform('/pages/product/view.html');
if (pathnameWithoutPlatformType.includes(pathnameInProductPage)) {
const productNo = searchParams.get('productNo');
if (productNo) {
return {
pageType: 'PRODUCT',
searchKey: 'productNo',
targetNo: productNo,
};
}
}
if (pathnameWithoutPlatformType === '/' || pathnameWithoutPlatformType.includes('/index')) {
return {
pageType: 'MAIN',
};
}
return null;
};
/**
* 데이터 가져오기
*
* @return {Promise<*[]|*>}
* @private
*/
const _getDisplayUrl = ({ pathnameWithoutPlatformType, pageTypeInformation }) => {
if (pageTypeInformation && pageTypeInformation.pageType !== 'MAIN') {
const { targetNo, searchKey } = pageTypeInformation;
return `${pathnameWithoutPlatformType}?${searchKey}=${targetNo}`;
}
return pathnameWithoutPlatformType;
};
const _getDesignPopups = async () => {
const cacheData = shopby.localStorage.getItemWithExpire(shopby.cache.key.designPopups.popups) || {};
const pathnameWithoutPlatformType = shopby.utils.getPathNameWithoutPlatform(location.pathname);
const pageTypeInformation = getPageTypeInformation({ pathnameWithoutPlatformType });
const displayUrl = _getDisplayUrl({ pathnameWithoutPlatformType, pageTypeInformation });
if (cacheData[displayUrl]) {
return cacheData[displayUrl];
}
const requestBody = {
displayUrl,
parameter: location.search.slice(1),
};
const { data } = await shopby.api.display.postDesignPopups({ requestBody });
let displayPopups = [];
if (pageTypeInformation) {
const displayPopupResponse = await shopby.api.display.getDisplayPopups({
queryString: { ...pageTypeInformation },
});
displayPopups = displayPopupResponse.data;
}
cacheData[displayUrl] = [...data, ...displayPopups];
shopby.localStorage.setItemWithExpire(shopby.cache.key.designPopups.popups, cacheData);
return cacheData[displayUrl];
};
/**
* 디자인 팝업 관련 소스 로드 (js, html)
*
* @return {Promise}
* @private
*/
const _loadDesignPopup = () => {
$('body').prepend('');
const $div = $('').appendTo('body');
const targets = ['design-popup-normal', 'design-popup-multi'];
return Promise.all(
targets.map(
target =>
new Promise(resolve => {
$('')
.appendTo($div)
.load(`/components/design-popup/${target}.html`, () => {
resolve();
});
}),
),
).then();
};
/**
* 디자인 팝업 생성
*
* @param popup
* @private
*/
const _createDesignPopup = popup => {
if (popup.detailInfo.screenType === 'WINDOW' && !shopby.isWindowPopup()) {
// window 팝업 호출
let windowOption = 'toolbar=no,status=no,statusbar=no,menubar=no,scrollbars=no,resizable=no';
windowOption += ',left=' + popup.detailInfo.screenLeftPosition;
windowOption += ',top=' + popup.detailInfo.screenTopPosition;
let windowWidth = popup.detailInfo.screenWidth + 35;
let windowHeight = popup.detailInfo.screenHeight + 110;
if (popup.popupDesignType === 'NORMAL') {
if (popup.detailInfo.screenWidthUnit === 'PERCENT') {
windowWidth = (window.innerWidth * popup.detailInfo.screenWidth) / 100;
}
if (popup.detailInfo.screenHeightUnit === 'PERCENT') {
windowHeight = (window.innerHeight * popup.detailInfo.screenHeight) / 100;
}
} else if (popup.popupDesignType === 'MULTI') {
windowWidth = popup.popupSlideInfo.slideMaxWidth + 64;
windowHeight = popup.popupSlideInfo.slideMaxHeight + 233;
}
windowOption += ',width=' + windowWidth;
windowOption += ',height=' + windowHeight;
// window.name 이 "null" 인 경우, 팝업에서 opener 를 제대로 세팅 못하는 이슈가 있음. 구글링해도 뭔지 잘 안나옴. window.name 을 지정하며 해결됨;;;
window.name = '';
window.open(
'/pages/popup/design-popup/design-popup.html?popupNo=' + popup.popupNo,
'disign-popup-' + popup.popupNo,
windowOption,
);
} else {
// 걍 일반 div 팝업
if (popup.popupDesignType === 'NORMAL') {
new shopby.designPopup.Normal(popup);
} else {
new shopby.designPopup.Multi(popup);
}
}
};
shopby.designPopup = {
initiate() {
if (shopby.isWindowPopup()) {
const popupNo = Number(shopby.utils.getUrlParam('popupNo'));
const popup = window.opener.shopby.designPopup.getPopupDataByPopupNo(popupNo);
if (!popup) {
window.close();
return;
}
_loadDesignPopup().then(() => {
_createDesignPopup(popup);
});
} else {
_getDesignPopups()
.then(data => {
designPopups = data.filter(popup => !invisibleToday.list.some(popupNo => popup.popupNo === popupNo));
if (designPopups.length === 0) {
return;
}
})
.then(_loadDesignPopup)
.then(() => {
designPopups.map(popup => {
_createDesignPopup(popup);
});
});
}
},
/**
* 팝업 데이터 가져오기 (by popupNo)
* - 윈도우 팝업 때문에 필요
*
* @param popupNo
* @return {*}
*/
getPopupDataByPopupNo(popupNo) {
if (!popupNo) {
return;
}
return designPopups.filter(popup => popup.popupNo === popupNo)[0];
},
/**
* 오늘 하루 보지 않기
*
* @param popupNo
*/
setInvisibleToday(popupNo) {
if (popupNo) {
invisibleToday.list.push(popupNo);
}
shopby.localStorage.setItem(shopby.cache.key.designPopups.invisibleToday, invisibleToday);
},
/**
* 멀티 팝업에서 클릭했을때, 메인 윈도우가 이동하기 위해
*
* @param targetUrl
* @param _blank
* @param bWindow
*/
onClickMultiPopup(targetUrl, _blank, bWindow) {
if (!targetUrl) {
return;
}
if (_blank) {
window.open(targetUrl, '_blank');
} else {
if (bWindow) {
window.opener.location.href = targetUrl;
} else {
location.href = targetUrl;
}
}
},
};
shopby.start.entries.push(shopby.designPopup.initiate.bind(shopby.designPopup));
})();
(() => {
shopby.helper.naverPay = ({ EMBED_ID, BUTTON_KEY, BUTTON_COUNT, ENABLE }) => {
return new NaverPay(EMBED_ID, BUTTON_KEY, BUTTON_COUNT, ENABLE);
};
class NaverPay {
constructor(EMBED_ID, BUTTON_KEY, BUTTON_COUNT, ENABLE) {
this.EMBED_ID = EMBED_ID; // append할 요소 id
this.BUTTON_KEY = BUTTON_KEY; // 네이버페이 버튼 키
this.BUTTON_COUNT = BUTTON_COUNT; // 버튼 개수 설정. 구매하기 버튼만 있으면 1, 찜하기 버튼도 있으면 2를 입력.
this.ENABLE = ENABLE; //
this.BUTTON_TYPE = 'A';
}
applyNaverPayButton(naverPayOrderHandler, naverWishlistHandler) {
if (!window.naver) {
throw new Error('네이버페이 버튼 스크립트를 로드해주세요.');
}
window.naver.NaverPayButton.apply({
EMBED_ID: this.EMBED_ID, // 임시로 넣은 마크업
BUTTON_KEY: this.BUTTON_KEY, // 페이에서 제공받은 버튼 인증 키 입력, 임시로 프로스킨꺼 삽입
TYPE: this.BUTTON_TYPE, // 버튼 모음 종류 설정
COLOR: 1, // 버튼 모음의 색 설정
COUNT: this.BUTTON_COUNT, // 버튼 개수 설정. 구매하기 버튼만 있으면 1, 찜하기 버튼도 있으면 2를 입력.
ENABLE: this.ENABLE, // 품절 등의 이유로 버튼 모음을 비활성화할 때에는 "N" 입력
BUY_BUTTON_HANDLER: naverPayOrderHandler,
WISHLIST_BUTTON_HANDLER: naverWishlistHandler,
});
}
requestNaverPayOrder(itemParam) {
if (!window.NCPPay) {
throw new Error('ncp_pay 스크립트를 로드해주세요.');
}
window.NCPPay.setConfiguration({
clientId: shopby.config.skin.clientId,
accessToken: shopby.cache.getAccessToken() || '',
platform: 'PC',
});
window.NCPPay.requestNaverPayOrder(
{
items: itemParam,
clientReturnUrl: location.href, // 화면 리턴될 url : 네이버페이에서 쇼핑몰로 이동 시 이 url로 이동됩니다.
},
() => {},
this.naverOrderErrorHandler,
);
}
naverOrderErrorHandler(error) {
const ERROR_CODE = {
SOLDOUT_OPTION: 'PPVE0011',
NO_EXHIBITION: 'PPVE0019',
UNAVAILABLE_PURCHASE_STATUS: 'NP0023',
FAIL_DELIVERY: 'NP0028',
};
if (!error || !error.code) return;
if ([ERROR_CODE.NO_EXHIBITION].includes(error.code)) {
shopby.goHome();
}
if (
[ERROR_CODE.SOLDOUT_OPTION, ERROR_CODE.UNAVAILABLE_PURCHASE_STATUS, ERROR_CODE.FAIL_DELIVERY].includes(
error.code,
)
) {
location.reload();
}
}
requestNaverWishlist(productNo) {
if (!window.NCPPay) {
throw new Error('ncp_pay 스크립트를 로드해주세요.');
}
window.NCPPay.setConfiguration({
clientId: shopby.config.skin.clientId,
platform: 'PC',
});
window.NCPPay.requestNaverPayWishList({
productNo,
clientReturnUrl: location.href,
});
}
}
})();
// 결제 관련 스크립트를 load
(() => {
const naverPayLoadTargets = ['/pages/product/view.html', '/pages/order/cart.html'];
const payLoadTargets = ['/pages/product/view.html', '/pages/order/cart.html', '/pages/order/order.html'];
const devHostKeywords = ['alpha-', 'localhost', 'devfe', 'gej0308'];
const environment = devHostKeywords.some(hostKeyword => location.hostname.includes(hostKeyword)) ? 'alpha' : 'real';
// deployInfo 객체는 어떻게 되는건지 모르겠지만, 일단 url로 하라고하니 여기다 결제 관련 스크립트 저장
const payScripts = {
real: [
'https://nsp.pay.naver.com/sdk/js/naverpay.min.js',
'https://shop-api.e-ncp.com/payments/ncp_pay.js',
'https://spay.kcp.co.kr/plugin/kcp_spay_hub.js',
'https://xpay.tosspayments.com/xpay/js/xpay_crossplatform.js',
'https://web.nicepay.co.kr/v3/webstd/js/nicepay-3.0.js',
],
alpha: [
'https://nsp.pay.naver.com/sdk/js/naverpay.min.js',
'https://alpha-shop-api.e-ncp.com/payments/ncp_pay_alpha.js',
'https://testspay.kcp.co.kr/plugin/kcp_spay_hub.js',
'https://xpay-sandbox.tosspayments.com/xpay/js/xpay_crossplatform.js',
'https://web.nicepay.co.kr/v3/webstd/js/nicepay-3.0.js',
],
};
const naverPayScript = {
real: {
pc: 'https://pay.naver.com/customer/js/naverPayButton.js',
mobile: 'https://pay.naver.com/customer/js/mobile/naverPayButton.js"',
},
alpha: {
pc: 'https://test-pay.naver.com/customer/js/naverPayButton.js',
mobile: 'https://test-pay.naver.com/customer/js/mobile/naverPayButton.js"',
},
};
loadPayScript();
loadNaverPayScript();
function loadPayScript() {
if (!payLoadTargets.includes(location.pathname)) {
return;
}
payScripts[environment].forEach(script =>
document.write(``),
);
}
function loadNaverPayScript() {
if (!naverPayLoadTargets.includes(location.pathname)) {
return;
}
document.write(``);
}
})();
// 캡챠
(() => {
shopby.helper.captcha = idName => new Captcha(idName);
class Captcha {
constructor(idName) {
this._mustAuthenticate(false);
this.render(idName);
this.errorKey = '';
}
render(idName) {
idName = idName ? idName : 'captcha';
this._$el = $(`#${idName}`);
this._render();
this._bindEvents();
}
reset() {
this._mustAuthenticate(false);
this._display(false);
}
retry(errorKey = '') {
this.errorKey = errorKey;
this._display(true);
}
async submitCode() {
if (this._$el.css('display') === 'none') return;
const code = this._$el.find('#captchaCode').val();
if (code === undefined) return;
this._validate(code);
try {
await shopby.api.auth.postCaptchaVerify();
} catch (e) {
throw new Error(e.message);
}
}
_mustAuthenticate(status) {
this._requiredAuth = status;
}
errorHandler(error) {
const captcha = 'captcha';
this._mustAuthenticate(true);
if (error.data) {
const { path, message } = error.data;
path.includes(captcha) && alert(message);
}
}
_validate(code) {
if (!code) {
shopby.alert('자동등록방지 문자를 입력해주세요.');
throw new Error('EMPTY_CODE');
}
}
async _render() {
await this._$el.html(this._getTemplate(''));
await this._display(this._requiredAuth);
}
async _fetchImage() {
const { data } = await shopby.api.auth.getCaptchaImage({
queryString: {
key: this.errorKey,
},
});
return data.url;
}
_getTemplate(url) {
return `
자동 입력 방지를 위해 아래 이미지의 문자 및 숫자를 순서대로 정확히 입력해주세요.
`;
}
async _setImgUrl() {
const url = await this._fetchImage();
this._$el.find('#captcha-img').attr('src', url);
}
async _display(show = true) {
show && (await this._setImgUrl());
this._$el.toggle(show);
}
_bindEvents() {
this._$el.on('click', '#refreshCaptcha', this._display.bind(this, true));
}
}
})();