(() => { const { EventManager, Handlebars, actions, utils, constants } = ShopbySkin; const API_BASE = 'https://shop-api.e-ncp.com'; const SECTION_LIST_API = `${API_BASE}/display/sections`; // sectionId가 MAIN-01, MAIN-02 ... 형태인 진열만 자동 노출 const MAIN_SECTION_ID_REGEX = /^MAIN-\d+$/i; // 상품 조회 쿼리 (관리자 진열순 / 품절포함 / 판매중) const PRODUCTS_QUERY = { by: 'ADMIN_SETTING', direction: 'DESC', soldout: true, saleStatus: 'ONSALE', pageNumber: 1, pageSize: 30, hasTotalCount: false, hasOptionValues: false, includeStopProduct: false, }; // sectionId 끝의 숫자 추출 (MAIN-01 → 1). 정렬 키로 사용. const getSectionOrder = (sectionId) => { const matched = String(sectionId).match(/\d+$/); return matched ? Number(matched[0]) : Number.MAX_SAFE_INTEGER; }; // clientId 확보: 이벤트(PAGE_LOAD_COMPLETED)에 의존하지 않고 직접 얻는다. // 느린 네트워크에서 partial JS가 늦게 로드되면 이벤트를 놓쳐 렌더가 통째로 안 되던 문제 방지. let clientIdPromise = null; const ensureClientId = () => { if (window.shopbyClientId) return Promise.resolve(window.shopbyClientId); if (clientIdPromise) return clientIdPromise; clientIdPromise = (async () => { // 1순위: 프레임워크 제공 환경 조회 (있을 때만) try { const env = await utils?.fetchEnvironment?.(); if (env?.clientId) return (window.shopbyClientId = env.clientId); } catch (e) { console.warn('[main-auto-sections] fetchEnvironment 실패, environment.json으로 폴백', e); } // 2순위: 정적 environment.json 직접 조회 const res = await fetch('/environment.json'); if (!res.ok) throw new Error(`environment.json 조회 실패: ${res.status}`); const env = await res.json(); return (window.shopbyClientId = env.clientId); })(); return clientIdPromise; }; const buildHeaders = () => { const headers = { accept: 'application/json', version: '1.0', clientid: window.shopbyClientId, platform: 'PC', language: 'ko', }; const accessToken = utils?.memberAuth?.get?.(); if (accessToken) { headers['shop-by-authorization'] = `Bearer ${accessToken}`; } return headers; }; // 노출 중인 진열 목록을 받아 MAIN-NN만 골라 번호 오름차순 정렬 const fetchMainSections = async () => { const res = await fetch(SECTION_LIST_API, { headers: buildHeaders() }); if (!res.ok) { throw new Error(`섹션 목록 조회 실패: ${res.status}`); } const data = await res.json(); const sections = data?.sections ?? []; return sections .filter((section) => MAIN_SECTION_ID_REGEX.test(section?.sectionId ?? '')) .sort((a, b) => getSectionOrder(a.sectionId) - getSectionOrder(b.sectionId)); }; // 진열 번호로 진열 내 상품 목록 조회 const fetchSectionProducts = async (sectionNo) => { const query = new URLSearchParams(PRODUCTS_QUERY).toString(); const res = await fetch(`${API_BASE}/display/sections/${sectionNo}/products?${query}`, { headers: buildHeaders(), }); if (!res.ok) { throw new Error(`상품 조회 실패(sectionNo=${sectionNo}): ${res.status}`); } const data = await res.json(); const products = data?.products ?? []; // 카드 템플릿이 쓰는 unescapedProductName은 API에 없으므로 직접 채워줌 // (display-product 웹컴포넌트가 내부적으로 하던 가공과 동일) return products.map((product) => ({ ...product, unescapedProductName: utils.unescapeHTML(product.productName ?? ''), })); }; // ────────────────────────────────────────────────────────────── // 상품 카드 클릭 액션 (display-product-new.js와 동일 동작) // ────────────────────────────────────────────────────────────── const productActionMap = { OPEN_PRODUCT_OPTION_MODAL: ({ event }) => { event?.preventDefault?.(); const btn = event.target.closest('[shopby-product-no]'); const productNo = Number(btn.getAttribute('shopby-product-no') ?? 0); const isSoldOut = utils.convertStringBooleanToBoolean(btn.getAttribute('shopby-is-sold-out')); if (isSoldOut) { return; } if (!utils.isAdultVerified(event)) { EventManager.fire('MODAL_CONFIRM_OPEN', { message: '성인인증이 필요한 상품입니다.', noticeType: constants.NOTICE_MODAL_TYPE.CAUTION, onConfirm: () => { location.href = `${location.origin}/pages/sign-in/sign-in.html?from=${encodeURIComponent(location.href)}`; }, }); return; } EventManager.fire('OPEN_LAYER_MODAL', { name: 'select-product-option', title: '옵션 선택', isFull: false, modalAddClass: 'select-product-option-modal', data: { productNo }, }); }, MOVE_PRODUCT_DETAIL: ({ event }) => { if (!utils.isAdultVerified(event)) { EventManager.fire('MODAL_CONFIRM_OPEN', { message: '성인인증이 필요한 상품입니다.', noticeType: 'CAUTION', onConfirm: () => { location.href = '/pages/intro/adult-certification.html?go-back=true'; }, }); } }, }; const handleClick = (e) => { const btn = e.target.closest('[shopby-action]') || e.target.parentElement?.closest('[shopby-action]'); const action = btn?.getAttribute('shopby-action'); productActionMap[action]?.({ event: e }); }; // ────────────────────────────────────────────────────────────── // 오늘출발 + 무료배송 뱃지 (display-product-new.js 로직 재사용) // ────────────────────────────────────────────────────────────── const applyBadges = (sectionEl, products, holidayData) => { const items = sectionEl.querySelectorAll('.thumb-item'); items.forEach((item, index) => { const product = products[index]; const badgeContainer = item.querySelector('.product-badges-container'); if (!badgeContainer || !product) return; badgeContainer.innerHTML = ''; const minFreeShippingAmt = product.deliveryConditionInfo?.criteria || 50000; let todayShippingBadgeHTML = ''; if (window.ShippingBadgeUtils && holidayData) { const canShip = window.ShippingBadgeUtils.canShipToday(holidayData.rawData, product); if (canShip) { todayShippingBadgeHTML = window.ShippingBadgeUtils.createTodayShippingBadge(); } else if (window.ShippingBadgeUtils.hasStock(product)) { const nextShippingDate = window.ShippingBadgeUtils.getNextShippingDate( holidayData.rawData, holidayData.continuousHoliday ); if (nextShippingDate) { todayShippingBadgeHTML = window.ShippingBadgeUtils.createNextShippingBadge( nextShippingDate.formatted ); } } } const isFreeShipping = product.deliveryConditionType === 'FREE' || product.salePrice - product.immediateDiscountAmt >= minFreeShippingAmt; const freeShippingBadgeHTML = isFreeShipping ? '무료배송' : ''; badgeContainer.innerHTML = todayShippingBadgeHTML + freeShippingBadgeHTML; }); }; // 섹션별 스와이퍼 초기화 (display-product-new.js와 동일 설정) const initSectionSwiper = (sectionEl) => { const swiperEl = sectionEl.querySelector('.swiper'); if (!swiperEl || swiperEl.swiper) return; new Swiper(swiperEl, { slidesPerView: 4, spaceBetween: 20, loop: true, speed: 800, observer: true, observeParents: true, navigation: { nextEl: sectionEl.querySelector('.swiper-button-next'), prevEl: sectionEl.querySelector('.swiper-button-prev'), }, autoplay: { delay: 3000, disableOnInteraction: false }, }); }; // ────────────────────────────────────────────────────────────── // 렌더링 // ────────────────────────────────────────────────────────────── let isRendered = false; const render = async () => { if (isRendered) return; const container = document.getElementById('main-auto-sections'); const tplEl = container?.querySelector('script[type="text/x-handlebars-template"]'); if (!container || !tplEl) { console.warn('[main-auto-sections] 컨테이너/템플릿을 찾지 못함'); return; } // 동기적으로 가드를 먼저 잡아 즉시 실행/이벤트 경로의 중복 렌더를 방지 isRendered = true; await ensureClientId(); const mainSections = await fetchMainSections(); if (mainSections.length === 0) { console.warn('[main-auto-sections] 노출할 MAIN-NN 진열이 없음'); isRendered = false; // 노출할 게 없었을 뿐이니 재시도 여지를 남겨둔다 return; } const template = Handlebars.compile(tplEl.innerHTML); // 각 진열의 상품을 조회해 순서대로 렌더 (번호순 보장 위해 순차 처리) for (const section of mainSections) { const products = await fetchSectionProducts(section.sectionNo); if (products.length === 0) continue; container.insertAdjacentHTML( 'beforeend', template({ label: section.sectionName, sectionId: section.sectionId, sectionNo: section.sectionNo, products, }) ); const sectionEl = container.querySelector( `.main-auto-section[data-section-no="${section.sectionNo}"]` ); sectionEl._products = products; initSectionSwiper(sectionEl); } container.addEventListener('click', handleClick); // 뱃지는 공휴일 데이터 로드 후 일괄 적용 const holidayData = window.ShippingBadgeUtils ? await window.ShippingBadgeUtils.getHolidayData() : null; container.querySelectorAll('.main-auto-section').forEach((sectionEl) => { applyBadges(sectionEl, sectionEl._products ?? [], holidayData); }); }; const start = () => render().catch((e) => console.error('[main-auto-sections]', e)); // 이벤트가 이미 지나갔어도(느린 네트워크) 렌더되도록 즉시 한 번 실행한다. start(); // 빠른 경로: 이벤트가 아직 안 왔다면 commonData의 clientId를 활용해 렌더한다. EventManager.on('PAGE_LOAD_COMPLETED', (commonData) => { if (commonData?.clientId) window.shopbyClientId = commonData.clientId; start(); }); })();