(() => {
const { pageHelper, EventManager, utils, actions } = ShopbySkin;
const eventDetailHelper = pageHelper.eventDetailHelper();
const containerEl = document.querySelector('[shopby-helper-key="event-detail-helper-key"]');
const { event: eventNoOrId } = utils.getQueryParams(location);
const eventDetailRequest = {
eventNo: eventNoOrId,
sortType: 'ADMIN_SETTING',
soldout: true,
};
const redirectLoginPage = () => {
const nextUrl = '/pages/sign-in/sign-in.html';
location.href = `${nextUrl}?from=${encodeURIComponent(location.href)}`;
};
eventDetailHelper.initialize({
helperKey: 'event-detail-helper-key',
});
const likeHelper = {
toggleClassName: (likeEl) => {
likeEl.classList.toggle('is-active');
},
toggleLikeStatus: async (likeEl) => {
const productNo = likeEl.getAttribute('shopby-product-no') ?? 0;
await actions.toggleLikeStatus({
productNo: Number(productNo),
});
},
};
const CLICK_EVENT_HANDLER_MAP = {
HANDLE_COUPON_BTN_CLICK: () => {
if (!utils.isSignIn()) {
ShopbySkin.EventManager.fire('MODAL_CONFIRM_OPEN', {
noticeType: 'CAUTION',
message: '로그인하셔야 본 서비스를 이용하실 수 있습니다.',
onConfirm: redirectLoginPage,
});
return;
}
const { coupon, eventNo } = document.querySelector('event-detail-product-list').store.state ?? {};
ShopbySkin.EventManager.fire('OPEN_LAYER_MODAL', {
title: '쿠폰받기',
name: 'coupon-download',
data: {
coupons: coupon?.coupons?.filter(({ downloadable }) => downloadable) ?? [],
eventNo,
},
isFull: false,
onClose: () => null,
});
},
HANDLE_SECTION_ID_CLICK: ({ target, helper }) => {
const tabId = target.getAttribute('shopby-tab-id');
helper.eventAction.selectEventCategory(tabId);
},
LIKE_PRODUCT: ({ target }) => {
if (!utils.isSignIn()) {
ShopbySkin.EventManager.fire('MODAL_CONFIRM_OPEN', {
noticeType: 'CAUTION',
message: '로그인하셔야 본 서비스를 이용하실 수 있습니다.',
onConfirm: redirectLoginPage,
});
return;
}
const likeEl = target.closest('[shopby-product-no]');
likeHelper.toggleClassName(likeEl);
likeHelper.toggleLikeStatus(likeEl);
},
};
const CHANGE_EVENT_HANDLER_MAP = {
HANDLE_SORT_CHANGE: ({ target, helper }) => {
helper.eventAction.fetchEventDetail({
...eventDetailRequest,
sortType: target.value,
});
},
};
EventManager.fire('INIT_EVENT_PRODUCT_SECTION_SLIDER', {
spaceBetween: 32,
direction: 'horizontal',
effect: 'slide',
slidesPerView: 'auto',
});
EventManager.on('PAGE_LOAD_COMPLETED', () => {
eventDetailHelper.eventAction.fetchEventDetail(eventDetailRequest);
});
const clickEventListener = ({ target }) => {
const actionType = target.getAttribute('shopby-action') ?? target.closest('button')?.getAttribute('shopby-action');
CLICK_EVENT_HANDLER_MAP[actionType]?.({
helper: eventDetailHelper,
target,
});
};
const changeEventListener = ({ target }) => {
const actionType = target.getAttribute('shopby-action');
CHANGE_EVENT_HANDLER_MAP[actionType]?.({
helper: eventDetailHelper,
target,
});
};
containerEl.addEventListener('click', clickEventListener);
containerEl.addEventListener('change', changeEventListener);
})();
/* ── 진열 섹션 스크롤형 노출 (전 기획전 · 섹션 2개 이상) ─────────────────────
섹션이 2개 이상인 기획전에서 탭 전환 대신 모든 섹션을 위아래로 나열하고,
상단 탭은 해당 섹션으로 스크롤 이동시킨다. 기획전 상세 응답에 전 섹션이 함께
내려오므로 추가 API 호출은 없다. 섹션 상품이 많으면 MORE_STEP 개씩 더보기로 노출. */
(() => {
const MORE_STEP = 20;
const SCROLL_GAP = 12;
const STICKY_TOP = null;
const SEL = {
list: 'event-detail-product-list[platform]',
grid: '.event-detail__products',
navBtn: '.event-nav__btn',
navBox: 'event-detail-display-menu',
total: '.total-count',
};
const container = document.querySelector('[shopby-helper-key="event-detail-helper-key"]');
if (!container) return;
const listEl = () => document.querySelector(SEL.list); // SDK가 현재 탭 상품을 그리는 원본 컴포넌트
const getState = () => listEl()?.store?.state ?? {};
const navBtns = () => [...container.querySelectorAll(SEL.navBtn)];
const renderedNos = () =>
[...(listEl()?.querySelectorAll(`${SEL.grid} .thumb-item[shopby-product-no]`) ?? [])].map((el) => el.getAttribute('shopby-product-no'));
const waitFor = (fn, timeout = 8000, interval = 50) =>
new Promise((resolve) => {
const started = Date.now();
const tick = () => {
let ok = false;
try {
ok = !!fn();
} catch (e) {
ok = false;
}
if (ok) return resolve(true);
if (Date.now() - started > timeout) return resolve(false);
setTimeout(tick, interval);
};
tick();
});
let topInset = 0;
const initialInset = () => {
if (STICKY_TOP !== null) return STICKY_TOP;
const header = document.querySelector('.shopby-header, header');
if (!header) return 0;
const { position } = getComputedStyle(header);
return position === 'sticky' || position === 'fixed' ? Math.round(header.getBoundingClientRect().height) : 0;
};
/* 화면 상단에 고정돼 있는 요소(헤더·띠배너 등)의 아래끝 = 탭바가 붙어야 할 위치.
스크롤 전에는 헤더가 원래 맨 위에 있어 구분이 안 되므로 스크롤된 뒤에만 측정한다. */
const measureTopInset = () => {
if (window.scrollY < 150) return null;
const navEl = container.querySelector(SEL.navBox);
const centerX = Math.round(window.innerWidth / 2);
let inset = 0;
// 띠배너 → 헤더처럼 고정요소가 겹겹이 쌓일 수 있어 아래끝을 따라 내려가며 잰다
for (let depth = 0; depth < 5; depth += 1) {
let next = inset;
document.elementsFromPoint(centerX, inset + 1).forEach((el) => {
if (el === navEl || navEl?.contains(el) || el.contains(navEl)) return;
const { position } = getComputedStyle(el);
if (position !== 'fixed' && position !== 'sticky') return;
next = Math.max(next, Math.round(el.getBoundingClientRect().bottom));
});
if (next === inset) break;
inset = next;
}
return inset;
};
const applyTopInset = (value) => {
if (value === null || value === topInset) return;
topInset = value;
document.documentElement.style.setProperty('--event-nav-top', `${topInset}px`);
};
const stickyTop = () => topInset;
/* 섹션 상품이 많을 때 MORE_STEP 개씩 끊어 보여준다 */
const applyMore = (block, grid) => {
const items = [...grid.querySelectorAll('.thumb-item')];
if (items.length <= MORE_STEP) return;
let shown = MORE_STEP;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'event-scroll__more';
btn.textContent = '더보기';
const render = () => {
items.forEach((item, i) => item.style.setProperty('display', i < shown ? '' : 'none'));
btn.hidden = shown >= items.length;
};
btn.addEventListener('click', () => {
shown = Math.min(shown + MORE_STEP, items.length);
render();
});
render();
block.appendChild(btn);
};
const buildSection = async (section, index, wrap) => {
const products = section.products ?? [];
if (!products.length) return true;
const expected = products.map(({ productNo }) => String(productNo));
navBtns()[index]?.click();
const rendered = await waitFor(() => {
const nos = renderedNos();
return nos.length === expected.length && nos.every((no, i) => no === expected[i]);
}, 5000);
if (!rendered) return false;
const grid = listEl()?.querySelector(SEL.grid);
if (!grid) return false;
const block = document.createElement('section');
block.className = 'event-scroll__section';
block.dataset.sectionIndex = String(index);
const title = document.createElement('h2');
title.className = 'event-scroll__title';
title.textContent = section.label ?? '';
const count = document.createElement('span');
count.className = 'event-scroll__count';
count.textContent = products.length;
title.appendChild(count);
const clonedGrid = grid.cloneNode(true);
block.appendChild(title);
block.appendChild(clonedGrid);
applyMore(block, clonedGrid);
wrap.appendChild(block); // 섹션이 완성되는 대로 바로 노출
return true;
};
const setActive = (index) => {
navBtns().forEach((btn, i) => btn.classList.toggle('is-active', i === index));
};
const bindTabs = (wrap) => {
const blocks = () => [...wrap.querySelectorAll('.event-scroll__section')];
const offsetTop = () => stickyTop() + (container.querySelector(SEL.navBox)?.getBoundingClientRect().height ?? 0) + SCROLL_GAP;
container.addEventListener(
'click',
(e) => {
const btn = e.target.closest?.(SEL.navBtn);
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const index = navBtns().indexOf(btn);
const target = blocks().find((block) => Number(block.dataset.sectionIndex) === index);
if (!target) return;
window.scrollTo({ top: window.scrollY + target.getBoundingClientRect().top - offsetTop(), behavior: 'smooth' });
// 스크롤 전에는 고정 헤더 높이를 알 수 없어, 이동 후 실제 값으로 한 번 보정한다
setTimeout(() => {
applyTopInset(measureTopInset());
const drift = Math.round(target.getBoundingClientRect().top - offsetTop());
if (Math.abs(drift) > 4) window.scrollTo({ top: window.scrollY + drift, behavior: 'smooth' });
}, 600);
},
true,
);
const syncActive = () => {
applyTopInset(measureTopInset());
const line = offsetTop() + 8;
const current = blocks().filter((block) => block.getBoundingClientRect().top <= line).pop();
setActive(current ? Number(current.dataset.sectionIndex) : 0);
};
window.addEventListener('scroll', syncActive, { passive: true });
syncActive();
};
const initialize = async () => {
const ready = await waitFor(
() => (getState().section ?? []).length > 1 && navBtns().length > 1 && listEl()?.querySelector(SEL.grid),
);
if (!ready) return;
const sections = getState().section ?? [];
const sdkList = listEl();
const wrap = document.createElement('div');
wrap.className = 'event-scroll';
sdkList.style.visibility = 'hidden';
sdkList.style.height = '0';
sdkList.style.overflow = 'hidden';
sdkList.parentNode.insertBefore(wrap, sdkList);
applyTopInset(initialInset());
document.body.classList.add('is-event-scroll');
container.querySelector(SEL.total)?.closest('event-detail-product-list')?.style.setProperty('display', 'none');
for (let i = 0; i < sections.length; i += 1) {
const ok = await buildSection(sections[i], i, wrap); // eslint-disable-line no-await-in-loop
if (ok) continue;
// 한 섹션이라도 렌더 확인이 안 되면 기존 탭형으로 되돌린다
wrap.remove();
document.body.classList.remove('is-event-scroll');
container.querySelector(SEL.total)?.closest('event-detail-product-list')?.style.removeProperty('display');
sdkList.style.removeProperty('visibility');
sdkList.style.removeProperty('height');
sdkList.style.removeProperty('overflow');
navBtns()[0]?.click();
return;
}
sdkList.style.display = 'none';
navBtns()[0]?.click(); // 섹션 순회로 밀린 탭 슬라이더를 처음으로 되돌린다
window.scrollTo({ top: 0 });
bindTabs(wrap);
};
initialize();
})();