(() => {
const ELEMENT_SELECTOR = {
HELPER_KEY: '[shopby-helper-key="cart-helper-key"]',
ACTION: 'shopby-action',
SELECTED_COUNT: 'shopby-selected-count',
CHECKED_CART_NOS_AS_STRING: 'shopby-checked-cart-nos-as-string',
DELIVERY_GROUP_IDX: 'shopby-delivery-group-idx',
IS_INVALID_PRODUCT: 'shopby-is-invalid-product',
CART_NO: 'shopby-cart-no',
CART_PRODUCTS: 'cart-products-v2',
CHECKBOX_PER_DELIVERY_GROUP_SLOT: '[slot="checkboxPerDeliveryGroup"]',
CHECKBOX_PER_CART_NO_SLOT: '[slot="checkboxPerCartNo"]',
};
const { pageHelper, EventManager, actions, utils } = ShopbySkin;
const containerEl = document.querySelector(ELEMENT_SELECTOR.HELPER_KEY);
const cartProductsEl = containerEl.querySelector(ELEMENT_SELECTOR.CART_PRODUCTS);
const cartHelper = pageHelper.cartHelper();
const FREE_SHIPPING_THRESHOLD = 50000;
let isCustomOrderCartChecked = false;
let latestCartPaymentInfo = null;
let isSyncingFreeShippingMessage = false;
let freeShippingRenderSyncTimer = null;
const hideCustomOrderCartUntilChecked = () => {
if (!cartProductsEl || isCustomOrderCartChecked) {
return;
}
cartProductsEl.style.visibility = 'hidden';
cartProductsEl.style.pointerEvents = 'none';
cartProductsEl.style.minHeight = '220px';
};
const showCustomOrderCartAfterChecked = () => {
isCustomOrderCartChecked = true;
if (!cartProductsEl) {
return;
}
cartProductsEl.style.visibility = '';
cartProductsEl.style.pointerEvents = '';
cartProductsEl.style.minHeight = '';
};
const getCartAmountNumber = (value) => Number(String(value ?? 0).replace(/[^\d.-]/g, '')) || 0;
const getRenderedCartPaymentInfo = () => {
const cartOrderStates = Array.from(containerEl.querySelectorAll('cart-order'))
.map((el) => el?.store?.state)
.filter(Boolean);
const stateWithPrice = cartOrderStates.find((state) => state.price);
return (
stateWithPrice?.price ||
stateWithPrice ||
cartOrderStates[0]?.price ||
cartOrderStates[0] ||
{}
);
};
const hasCartPaymentAmount = (paymentInfo = {}) =>
['standardAmt', 'buyAmt', 'totalAmt', 'totalDeliveryAmt', 'totalPrePaidDeliveryAmt'].some(
(key) => getCartAmountNumber(paymentInfo[key]) > 0
);
const getCurrentCartPaymentInfo = () => {
const latestPaymentInfo = latestCartPaymentInfo?.price || latestCartPaymentInfo || {};
return hasCartPaymentAmount(latestPaymentInfo)
? latestPaymentInfo
: getRenderedCartPaymentInfo();
};
const getRenderedFreeShippingProgressPaymentInfo = (el) => {
const priceTagEl =
el.closest('.cart-list__price-tag, section')?.querySelector('.price-tag') ||
containerEl.querySelector('.price-tag');
const getAmount = (selector) =>
getCartAmountNumber(priceTagEl?.querySelector(selector)?.textContent);
const standardAmt = getAmount('[shopby-free-shipping-standard-amt]');
const discountAmt = getAmount('[shopby-free-shipping-discount-amt]');
const deliveryAmt = getAmount('[shopby-free-shipping-delivery-amt]');
const totalAmt = getAmount('[shopby-free-shipping-total-amt]');
return {
standardAmt,
discountAmt,
buyAmt: Math.max(0, totalAmt - deliveryAmt),
totalAmt,
totalDeliveryAmt: deliveryAmt,
totalPrePaidDeliveryAmt: deliveryAmt,
totalPayOnDeliveryAmt: 0,
};
};
const getFreeShippingProgressPaymentInfo = (el) => {
const datasetPaymentInfo = {
standardAmt: el.dataset.standardAmt,
discountAmt: el.dataset.discountAmt,
buyAmt: el.dataset.buyAmt,
totalAmt: el.dataset.totalAmt,
totalDeliveryAmt: el.dataset.totalDeliveryAmt,
totalPrePaidDeliveryAmt: el.dataset.totalPrepaidDeliveryAmt,
totalPayOnDeliveryAmt: el.dataset.totalPayOnDeliveryAmt,
};
const currentPaymentInfo = getCurrentCartPaymentInfo();
const renderedPaymentInfo = getRenderedFreeShippingProgressPaymentInfo(el);
if (hasCartPaymentAmount(renderedPaymentInfo)) {
return renderedPaymentInfo;
}
return hasCartPaymentAmount(currentPaymentInfo) ? currentPaymentInfo : datasetPaymentInfo;
};
const scheduleFreeShippingMessageSync = () => {
requestAnimationFrame(syncFreeShippingMessage);
setTimeout(syncFreeShippingMessage, 100);
setTimeout(syncFreeShippingMessage, 300);
setTimeout(syncFreeShippingMessage, 500);
setTimeout(syncFreeShippingMessage, 1000);
};
const syncFreeShippingMessage = () => {
isSyncingFreeShippingMessage = true;
const freeShippingMessageEls = containerEl.querySelectorAll('[shopby-free-shipping-message]');
const freeShippingProgressEls = containerEl.querySelectorAll('[shopby-free-shipping-progress]');
let hasFreeShipping = false;
freeShippingProgressEls.forEach((el) => {
const paymentInfo = getFreeShippingProgressPaymentInfo(el);
const standardAmt = getCartAmountNumber(paymentInfo.standardAmt);
const discountAmt = getCartAmountNumber(paymentInfo.discountAmt);
const buyAmt = getCartAmountNumber(paymentInfo.buyAmt);
const totalAmt = getCartAmountNumber(paymentInfo.totalAmt);
const totalDeliveryAmt = getCartAmountNumber(paymentInfo.totalDeliveryAmt);
const totalPrePaidDeliveryAmt = getCartAmountNumber(paymentInfo.totalPrePaidDeliveryAmt);
const totalPayOnDeliveryAmt = getCartAmountNumber(paymentInfo.totalPayOnDeliveryAmt);
const deliveryAmt = Math.max(
totalDeliveryAmt,
totalPrePaidDeliveryAmt,
totalPayOnDeliveryAmt
);
const productAmt =
Math.max(0, standardAmt - discountAmt) ||
Math.max(0, buyAmt - deliveryAmt) ||
Math.max(0, totalAmt - deliveryAmt);
const hasPayableProduct = standardAmt > 0 || buyAmt > 0 || totalAmt > 0 || productAmt > 0;
const remainingAmt = Math.max(0, FREE_SHIPPING_THRESHOLD - productAmt);
const progressRate = hasPayableProduct
? Math.min(100, Math.max(4, (productAmt / FREE_SHIPPING_THRESHOLD) * 100))
: 0;
const isFreeShipping = hasPayableProduct && deliveryAmt === 0;
const titleEl = el.querySelector('[shopby-free-shipping-progress-title]');
const barEl = el.querySelector('[shopby-free-shipping-progress-bar]');
el.hidden = false;
hasFreeShipping = hasFreeShipping || isFreeShipping;
if (!hasPayableProduct) {
el.hidden = true;
if (barEl) {
barEl.style.width = '0%';
}
return;
}
if (titleEl) {
const message =
remainingAmt > 0
? `무료배송까지 ${remainingAmt.toLocaleString()}원 남았어요`
: '무료배송 조건을 달성했어요';
if (titleEl.textContent !== message) {
titleEl.textContent = message;
}
}
if (barEl) {
const progressWidth = `${progressRate}%`;
if (barEl.style.width !== progressWidth) {
barEl.style.width = progressWidth;
}
}
});
freeShippingMessageEls.forEach((el) => {
el.hidden = !hasFreeShipping;
});
isSyncingFreeShippingMessage = false;
};
const observeFreeShippingRender = () => {
if (!window.MutationObserver) {
return;
}
const observer = new MutationObserver(() => {
if (isSyncingFreeShippingMessage) {
return;
}
clearTimeout(freeShippingRenderSyncTimer);
freeShippingRenderSyncTimer = setTimeout(syncFreeShippingMessage, 50);
});
observer.observe(containerEl, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
});
};
const bindFreeShippingInstantSync = () => {
['click', 'input', 'change'].forEach((eventName) => {
containerEl.addEventListener(eventName, scheduleFreeShippingMessageSync, true);
});
};
hideCustomOrderCartUntilChecked();
observeFreeShippingRender();
bindFreeShippingInstantSync();
setTimeout(showCustomOrderCartAfterChecked, 5000);
cartHelper.initialize({
helperKey: 'cart-helper-key',
});
const handleOrder = async ({ helper, cartNos, products }) => {
const {
data: { orderSheetNo },
} = await helper.orderSheetAction.makeOrderSheet({
cartNos,
products,
});
const ordersheetUri = `/pages/order/order-sheet-form.html?ordersheetNo=${orderSheetNo}`;
if (utils.isSignedIn()) {
location.href = ordersheetUri;
} else {
const url = new URL(location.href);
url.pathname = '/pages/sign-in/sign-in.html';
url.searchParams.set('from', encodeURIComponent(ordersheetUri));
location.href = url.toString();
}
};
const CLICK_EVENT_HANDLER_MAP = {
TOGGLE_ALL: ({ helper, target }) => {
helper.cartAction.updateIsAllChecked(target.checked);
},
TOGGLE_CHECKBOX_PER_DELIVERY_GROUP: ({ helper, target }) => {
const labelEl = target.closest(ELEMENT_SELECTOR.CHECKBOX_PER_DELIVERY_GROUP_SLOT);
const deliveryGroupIdx = Number(
labelEl.getAttribute(ELEMENT_SELECTOR.DELIVERY_GROUP_IDX) ?? -1
);
const isInvalidProduct = labelEl.getAttribute(ELEMENT_SELECTOR.IS_INVALID_PRODUCT) === 'true';
helper.cartAction.updateIsDeliveryGroupChecked({
deliveryGroupIdx: isInvalidProduct ? -1 : deliveryGroupIdx,
isChecked: target.checked,
});
},
TOGGLE_CHECKBOX_PER_CART_NO: ({ helper, target }) => {
const labelEl = target.closest(ELEMENT_SELECTOR.CHECKBOX_PER_CART_NO_SLOT);
helper.cartAction.updateIsCartNoChecked({
cartNo: Number(labelEl.getAttribute(ELEMENT_SELECTOR.CART_NO) ?? 0),
isChecked: target.checked,
});
},
DELETE_SELECTED_ITEMS: async ({ helper, target }) => {
const selectedCount = Number(target.getAttribute(ELEMENT_SELECTOR.SELECTED_COUNT));
const checkedCartNos =
target
.getAttribute(ELEMENT_SELECTOR.CHECKED_CART_NOS_AS_STRING)
?.split?.(',')
?.map(Number) ?? [];
if (!selectedCount) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message: `선택하신 상품이 없습니다.`,
});
return;
}
const { data } = await helper.cartAction.deletableRequiredOption();
if (!data) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message:
'선택옵션은 단독 주문이 불가합니다. 필수옵션 취소 필요 시 선택옵션을 먼저 취소해주시거나, 함께 취소해주세요.',
});
return;
}
EventManager.fire('MODAL_CONFIRM_OPEN', {
noticeType: 'WARNING',
message: `선택하신 ${selectedCount}개의 상품을 삭제하시겠습니까?`,
confirmLabel: '삭제',
onConfirm: async () => {
EventManager.fire('UPDATE_CART_PRODUCT');
await helper.cartAction.deleteCartNos(checkedCartNos);
helper.cartAction.fetchCartDetail();
},
});
},
ORDER: ({ helper }) => {
const { data: checkedCartNos } = helper.cartAction.fetchCheckedCartNos();
const { data: checkedProducts } = helper.cartAction.fetchCheckedProducts();
const invalidCartNos = Array.from(
containerEl.querySelectorAll('[slot="deliveryGroup"][shopby-delivery-group-idx="-1"]')
).flatMap((el) => {
const cartNoEls = el.querySelectorAll('[shopby-cart-no]');
return Array.from(cartNoEls).flatMap((el) => Number(el.getAttribute('shopby-cart-no')));
});
const hasInvalidCartNo = invalidCartNos.some((no) => checkedCartNos.includes(no));
const editingEl = containerEl.querySelector('[shopby-status="EDITING"]');
if (editingEl) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message: `변경중인 옵션이 있습니다.`,
onClose: () => {
editingEl?.focus?.();
},
});
return;
}
if (!checkedCartNos.length) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message: `선택된 상품이 없습니다.`,
});
return;
}
if (hasInvalidCartNo) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message: `선택하신 상품에 구매불가한 상품이 있습니다.`,
});
return;
}
handleOrder({
cartNos: checkedCartNos,
products: checkedProducts,
helper,
});
},
ORDER_ALL: async ({ helper }) => {
const editingEl = containerEl.querySelector('[shopby-status="EDITING"]');
if (editingEl) {
EventManager.fire('MODAL_ALERT_OPEN', {
noticeType: 'CAUTION',
message: `변경중인 옵션이 있습니다.`,
onClose: () => {
editingEl?.focus?.();
},
});
return;
}
const { data } = await helper.cartAction.fetchAllValidProducts();
const cartNos = data.map(({ cartNo }) => cartNo);
handleOrder({
cartNos,
products: data,
helper,
});
},
};
const clickEventListener = ({ target }) => {
const actionType = target.getAttribute(ELEMENT_SELECTOR.ACTION);
CLICK_EVENT_HANDLER_MAP[actionType]?.({
target,
helper: cartHelper,
});
};
containerEl.addEventListener('click', clickEventListener);
// 네이버 주문
const naverPay = {
action: null,
usable: false,
el: document.querySelector('#naver-pay'),
getVisibility: (visible) => (visible ? 'visible' : 'hidden'),
getTotalCount: (data) => Object.keys(data)?.length || 0,
handleReceiverCheckingStatus: (data) => {
const { el, action, getTotalCount, getVisibility, usable } = naverPay;
const totalCount = getTotalCount(data);
const showsNaverPayButton = usable && totalCount > 0;
const visibility = getVisibility(showsNaverPayButton);
if (el) {
el.style.visibility = visibility;
return;
}
if (!showsNaverPayButton) {
return;
}
action.showNaverPayButton({
containerElementId: 'naver-pay',
isCartPage: true,
});
},
handleReceiverCheckedProducts: (data) => {
const items = data.map(({ productNo, optionNo, orderCnt, optionInputs, channelType }) => ({
productNo,
optionNo,
orderCnt,
optionInputs,
channelType: channelType || utils.getQueryParams()?.channelType,
}));
naverPay.action.prepareNaverPay({ items });
},
setNaverPayAction: async ({ env, clientId, orderConfig }) => {
naverPay.action = await actions.getNaverPayAction({
mallProfile: env,
clientId,
platform: utils.isMobileDevice ? 'MOBILE_WEB' : 'PC',
});
naverPay.usable = Boolean(orderConfig.naverPay);
},
};
const CUSTOM_ORDER_QUANTITY_SUFFIX = '\uAC1C';
const CUSTOM_ORDER_PRODUCT_NAME_LABEL = '\uC0C1\uD488\uBA85';
const CUSTOM_ORDER_QUANTITY_LABELS = [
'\uAD6C\uB9E4\uAC1C\uC218',
'\uAD6C\uB9E4\uAC2F\uC218',
'\uAD6C\uB9E4\uC218\uB7C9',
'\uAC1C\uC218',
'\uC218\uB7C9',
];
let isCustomOrderCartDedupeRunning = false;
const normalizeCustomOrderCompareValue = (value) =>
String(value ?? '')
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
const normalizeCustomOrderInputLabel = (label) =>
String(label ?? '')
.replace(/\s+/g, '')
.trim();
const getCustomOrderInputLabel = (input) =>
input?.inputLabel || input?.label || input?.optionName || '';
const getCustomOrderInputValue = (input) => {
if (!input) {
return '';
}
if (Array.isArray(input.inputValues)) {
return input.inputValues.filter(Boolean).join(' ');
}
return input.inputValue || input.value || '';
};
const isCustomOrderQuantityLabel = (label) => {
const normalizedLabel = normalizeCustomOrderInputLabel(label);
return CUSTOM_ORDER_QUANTITY_LABELS.some((keyword) => normalizedLabel.includes(keyword));
};
const getCustomOrderProductNameInput = (optionInputs = []) => {
const inputs = Array.isArray(optionInputs) ? optionInputs : [];
return (
inputs.find((input) =>
normalizeCustomOrderInputLabel(getCustomOrderInputLabel(input)).includes(
CUSTOM_ORDER_PRODUCT_NAME_LABEL
)
) ||
inputs.find((input) => !isCustomOrderQuantityLabel(getCustomOrderInputLabel(input))) ||
inputs[0] ||
null
);
};
const getCustomOrderProductNameKey = (item) =>
normalizeCustomOrderCompareValue(
getCustomOrderInputValue(getCustomOrderProductNameInput(item?.optionInputs))
);
const getCustomOrderCartMergeKey = (item) => {
const productNameKey = getCustomOrderProductNameKey(item);
const itemProductNo = Number(item?.productNo);
const itemOptionNo = Number(item?.optionNo);
if (!productNameKey || !itemProductNo || !itemOptionNo) {
return '';
}
return `${itemProductNo}::${itemOptionNo}::${productNameKey}`;
};
const getCustomOrderQuantityText = (quantity) =>
`${Math.max(1, Number(quantity) || 1)}${CUSTOM_ORDER_QUANTITY_SUFFIX}`;
const updateCustomOrderQuantityInputs = (optionInputs = [], quantity) => {
const inputs = Array.isArray(optionInputs) ? optionInputs : [];
const quantityText = getCustomOrderQuantityText(quantity);
let updatedQuantityInput = false;
const nextInputs = inputs.map((input, index) => {
if (!isCustomOrderQuantityLabel(getCustomOrderInputLabel(input)) && index !== 1) {
return input;
}
updatedQuantityInput = true;
return {
...input,
inputValue: quantityText,
inputValues: Array.isArray(input.inputValues) ? [quantityText] : input.inputValues,
};
});
return updatedQuantityInput ? nextInputs : inputs;
};
const collectCartProducts = (
value,
products = [],
visitedObjects = new Set(),
cartNos = new Set()
) => {
if (!value || typeof value !== 'object' || visitedObjects.has(value)) {
return products;
}
visitedObjects.add(value);
if (Array.isArray(value)) {
value.forEach((item) => collectCartProducts(item, products, visitedObjects, cartNos));
return products;
}
const cartNo = Number(value.cartNo);
if (
cartNo &&
!cartNos.has(cartNo) &&
Number(value.productNo) &&
Number(value.optionNo) &&
value.orderCnt != null
) {
products.push(value);
cartNos.add(cartNo);
}
Object.keys(value).forEach((key) =>
collectCartProducts(value[key], products, visitedObjects, cartNos)
);
return products;
};
const dedupeCustomOrderCartProducts = async (cart) => {
if (isCustomOrderCartDedupeRunning) {
return;
}
const cartGroups = new Map();
collectCartProducts(cart).forEach((cartItem) => {
const mergeKey = getCustomOrderCartMergeKey(cartItem);
if (!mergeKey || !cartItem.cartNo) {
return;
}
cartGroups.set(mergeKey, [...(cartGroups.get(mergeKey) || []), cartItem]);
});
const itemsToModify = [];
const cartNosToDelete = [];
cartGroups.forEach((cartItems) => {
if (cartItems.length < 2) {
return;
}
const [targetItem, ...duplicateItems] = cartItems;
const nextOrderCnt = cartItems.reduce(
(sum, item) => sum + Math.max(1, Number(item.orderCnt) || 1),
0
);
itemsToModify.push({
cartNo: targetItem.cartNo,
orderCnt: nextOrderCnt,
optionInputs: updateCustomOrderQuantityInputs(targetItem.optionInputs, nextOrderCnt),
groupId: targetItem.groupId ?? null,
});
cartNosToDelete.push(...duplicateItems.map((item) => item.cartNo).filter(Boolean));
});
if (itemsToModify.length === 0 && cartNosToDelete.length === 0) {
showCustomOrderCartAfterChecked();
return;
}
isCustomOrderCartDedupeRunning = true;
cartProductsEl?.showLoadingSpinner?.();
try {
if (cartNosToDelete.length > 0) {
await cartHelper.cartAction.deleteCartNos(cartNosToDelete);
}
if (itemsToModify.length > 0) {
await cartHelper.cartAction.modifyCart(itemsToModify);
}
await cartHelper.cartAction.fetchCartDetail();
} catch (error) {
console.warn('Failed to merge duplicate custom-order cart products.', error);
} finally {
isCustomOrderCartDedupeRunning = false;
cartProductsEl?.hideLoadingSpinner?.();
showCustomOrderCartAfterChecked();
}
};
EventManager.on('UPDATE_CART_PRODUCT', cartProductsEl?.showLoadingSpinner);
EventManager.on('ON_UPDATED_CART_PRODUCT', cartProductsEl?.hideLoadingSpinner);
EventManager.on('ON_UPDATED_CART_PRODUCT', scheduleFreeShippingMessageSync);
EventManager.on('QUERY_CART_CHECKED_PRODUCTS', naverPay.handleReceiverCheckedProducts);
EventManager.on('QUERY_CART_CHECKING_STATUS_PER_CART_NO', naverPay.handleReceiverCheckingStatus);
EventManager.on('QUERY_CART_PAYMENT_INFO', (paymentInfo) => {
latestCartPaymentInfo = paymentInfo || {};
scheduleFreeShippingMessageSync();
});
EventManager.on('PAGE_LOAD_COMPLETED', (commonData) => {
cartHelper.cartAction.fetchCartDetail();
naverPay.setNaverPayAction(commonData);
scheduleFreeShippingMessageSync();
// 외부스크립트 등록 기능. 삭제금지
ShopbyExternalScript?.setPageScriptType('CART');
});
EventManager.on('QUERY_CART_DETAIL', (cart) => {
dedupeCustomOrderCartProducts(cart);
scheduleFreeShippingMessageSync();
// 외부스크립트 등록 기능. 삭제금지
ShopbyExternalScript?.setGlobalObjectSb({ cart });
});
})();