(() => { 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 }); }); })();