(() => { 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(); 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); }, }; EventManager.on('UPDATE_CART_PRODUCT', cartProductsEl?.showLoadingSpinner); EventManager.on('ON_UPDATED_CART_PRODUCT', cartProductsEl?.hideLoadingSpinner); EventManager.on('QUERY_CART_CHECKED_PRODUCTS', naverPay.handleReceiverCheckedProducts); EventManager.on('QUERY_CART_CHECKING_STATUS_PER_CART_NO', naverPay.handleReceiverCheckingStatus); EventManager.on('PAGE_LOAD_COMPLETED', (commonData) => { cartHelper.cartAction.fetchCartDetail(); naverPay.setNaverPayAction(commonData); // 외부스크립트 등록 기능. 삭제금지 ShopbyExternalScript?.setPageScriptType('CART'); }); EventManager.on('QUERY_CART_DETAIL', (cart) => { // 외부스크립트 등록 기능. 삭제금지 ShopbyExternalScript?.setGlobalObjectSb({ cart }); }); // ── 무료배송 안내 + 그룹 배송비 (추가 기능, 실패 시 조용히 숨김) ────── // 배송그룹이 1개면 상단 바, 2개 이상이면 상단 바를 숨기고 그룹 헤더 아래에 // 같은 디자인의 축소 바를 그룹마다 붙인다. // 배송비는 상품행이 아니라 그룹 헤더 우측에 "선택된 상품" 기준으로 표시해 // 하단 결제금액의 배송비 합계와 항상 같은 값이 되게 한다. (function freeShip() { const barEl = document.getElementById('gbFreeShip'); if (!barEl) return; const msgEl = barEl.querySelector('.gb-freeship__msg'); const fillEl = barEl.querySelector('.gb-freeship__fill'); const MINI_CLASS = 'gb-freeship--mini'; const FEE_CLASS = 'gb-group-fee'; const HAS_FEE_CLASS = 'gb-freeship-hasfree'; let groups = []; // 배송그룹별 { idx, free, threshold, baseAmt, payType, amtByCartNo } let keyIndex = {}; // productNo_optionNo → { groupIdx, amt } (cartNo 누락 대비) let checkedSet = null; // 선택된 cartNo Set (null=미초기화) let unitByCartNo = {}; // cartNo → 1개 금액 (수량 변경 즉시 반영용) const won = (n) => (n || 0).toLocaleString('ko-KR'); function rebuild(cart) { groups = []; keyIndex = {}; unitByCartNo = {}; ((cart && cart.deliveryGroups) || []).forEach((g, idx) => { const cond = g.deliveryCondition || {}; const conditional = cond.deliveryConditionType === 'CONDITIONAL' && cond.aboveDeliveryAmt > 0; const first = (g.orderProducts || [])[0] || {}; const group = { idx, free: cond.deliveryConditionType === 'FREE', threshold: conditional ? cond.aboveDeliveryAmt : 0, baseAmt: cond.baseDeliveryAmt || 0, apiAmt: g.deliveryAmt || 0, payType: g.deliveryPayType || first.deliveryPayType || '', notDeliverable: !!g.isAllNotDeliverable, amtByCartNo: {}, }; (g.orderProducts || []).forEach((op) => { (op.orderProductOptions || []).forEach((opt) => { const amt = (opt.price && (opt.price.buyAmt != null ? opt.price.buyAmt : opt.price.salePrice)) || 0; if (opt.cartNo != null) { group.amtByCartNo[opt.cartNo] = amt; if (opt.orderCnt > 0) unitByCartNo[opt.cartNo] = amt / opt.orderCnt; } if (opt.productNo != null && opt.optionNo != null) { keyIndex[opt.productNo + '_' + opt.optionNo] = { groupIdx: idx, amt }; } }); }); groups.push(group); }); } function subtotal(group) { if (!checkedSet) { // 미초기화(최초 로드): 그 그룹에 담긴 금액 합 return Object.keys(group.amtByCartNo).reduce((s, k) => s + group.amtByCartNo[k], 0); } let sum = 0; checkedSet.forEach((no) => { sum += group.amtByCartNo[no] || 0; }); return sum; } function hasSelection(group) { if (!checkedSet) return Object.keys(group.amtByCartNo).length > 0; let found = false; checkedSet.forEach((no) => { if (group.amtByCartNo[no] != null) found = true; }); return found; } // 그룹 헤더에 표시할 배송비 — 선택된 상품 기준이라 하단 합계와 값이 같다 function feeInfo(group) { if (group.notDeliverable || !hasSelection(group)) return null; const pay = group.payType === 'PAY_ON_DELIVERY' ? ' (착불)' : ' (선결제)'; if (group.free) return { free: true, text: '무료배송' }; if (group.threshold) { if (subtotal(group) >= group.threshold) return { free: true, text: '무료배송' }; return { free: false, text: '배송비 ' + won(group.baseAmt) + '원' + pay }; } if (!group.apiAmt) return { free: true, text: '무료배송' }; return { free: false, text: '배송비 ' + won(group.apiAmt) + '원' + pay }; } // 문구·아이콘·진행바는 상단 바와 축소 바가 동일 (축소 바만 뒤 문장 생략) function msgHtml(remain, mini) { if (remain <= 0) return '무료배송 적용!' + (mini ? '' : ' 지금 주문하면 배송비가 무료예요.'); return '' + won(remain) + '원만 더 담으면 무료배송'; } const percent = (sel, threshold) => Math.max(0, Math.min(100, Math.round((sel / threshold) * 100))); function renderTopBar() { // 배송그룹이 1개일 때만 상단 바 (2개 이상은 그룹별 축소 바로 대체) const only = groups.length === 1 ? groups[0] : null; if (!only || !only.threshold) { barEl.hidden = true; return; } const sel = subtotal(only); const remain = only.threshold - sel; fillEl.style.width = percent(sel, only.threshold) + '%'; barEl.classList.toggle('is-done', remain <= 0); msgEl.innerHTML = msgHtml(remain, false); barEl.hidden = false; } function miniBar(sel, threshold) { const remain = threshold - sel; const el = document.createElement('div'); el.className = 'gb-freeship ' + MINI_CLASS + (remain <= 0 ? ' is-done' : ''); el.innerHTML = '
' + msgHtml(remain, true) + '
' + '