(() => {
const { EventManager, utils } = ShopbySkin;
// --- API Helper ---
const fetchCategoriesFromApi = async () => {
try {
console.log("Fetching categories from API...");
// Use verified Client ID
const clientId = 'UUBsnIVqbs6h156TJHMiRA==';
const response = await fetch('https://shop-api.e-ncp.com/categories', {
headers: {
'clientid': clientId,
'version': '1.0',
'content-type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
return data.multiLevelCategories || [];
}
} catch (e) {
console.error("Failed to fetch categories from API", e);
}
return [];
};
// --- Main Logic ---
// State
const state = {
categories: [],
selectedDepth1: null,
activeDepth2No: null // Currently visible/active section
};
// Elements
const els = {
tabs: document.getElementById('category-tabs-list'),
sidebar: document.getElementById('category-sidebar-list'),
grid: document.getElementById('category-content-grid'), // Main scroll area
};
// Utils
const safeHtml = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
// --- ScrollSpy Logic ---
let observer = null;
let isScrollingFromClick = false; // Flag to disable spy during click scroll
const setupScrollSpy = () => {
if (observer) observer.disconnect();
// Observe sections to update sidebar active state
const options = {
root: els.grid.parentElement, // .category-grid-area
rootMargin: '-10% 0px -70% 0px', // Trigger when section is near top
threshold: 0
};
observer = new IntersectionObserver((entries) => {
if (isScrollingFromClick) return;
// Find the visible section
let visibleSection = null;
entries.forEach(entry => {
if (entry.isIntersecting) {
visibleSection = entry.target;
}
});
if (visibleSection) {
const no = Number(visibleSection.dataset.no);
updateSidebarActive(no);
}
}, options);
const sections = document.querySelectorAll('.category-section-wrap');
sections.forEach(section => observer.observe(section));
};
const updateSidebarActive = (no) => {
if (state.activeDepth2No === no) return;
state.activeDepth2No = no;
// Visual update
const prev = els.sidebar.querySelector('.active');
if (prev) prev.classList.remove('active');
const next = els.sidebar.querySelector(`.category-sidebar-item[data-no="${no}"]`);
if (next) {
next.classList.add('active');
// Auto-scroll sidebar to keep active item in view
next.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
// --- Render Functions ---
// 1. Render Top Tabs (Depth 1)
const renderTabs = () => {
if (!els.tabs) return;
els.tabs.innerHTML = state.categories.map(cat => {
const isActive = state.selectedDepth1 && state.selectedDepth1.categoryNo === cat.categoryNo;
return `
${safeHtml(cat.label)}`;
}).join('');
};
// 2. Render Sidebar (Depth 2 List)
const renderSidebar = () => {
if (!els.sidebar || !state.selectedDepth1) return;
const children = state.selectedDepth1.children || [];
// Show all Depth 2 items as sidebar links
if (children.length === 0) {
els.sidebar.innerHTML = '';
return;
}
els.sidebar.innerHTML = children.map((cat, idx) => {
// Active state defaults to first one initially if null
const isActive = (state.activeDepth2No === cat.categoryNo) || (!state.activeDepth2No && idx === 0);
return ``;
}).join('');
};
// 3. Render Content (All Sections)
const renderContent = () => {
if (!els.grid || !state.selectedDepth1) return;
const depth2List = state.selectedDepth1.children || [];
els.grid.innerHTML = '';
if (depth2List.length === 0) {
els.grid.innerHTML = '카테고리가 없습니다.
';
return;
}
const html = depth2List.map(d2 => {
const d3List = d2.children || [];
// Grid Content: Depth 3 Text Links
let gridHtml = '';
if (d3List.length > 0) {
// 2-column text grid
gridHtml = d3List.map(d3 => `
${safeHtml(d3.label)}
`).join('');
} else {
// No sub-categories? Just link to itself
gridHtml = `
${safeHtml(d2.label)} 전체보기
`;
}
return `
`;
}).join('');
els.grid.innerHTML = html;
// Reset
els.grid.parentElement.scrollTop = 0;
// Init active state
if (depth2List.length > 0) {
state.activeDepth2No = depth2List[0].categoryNo;
}
// Delay spy setup to allow DOM update
setTimeout(setupScrollSpy, 200);
};
// --- Handlers ---
const handleTabClick = (no) => {
if (state.selectedDepth1?.categoryNo === no) return;
const cat = state.categories.find(c => c.categoryNo === no);
if (cat) {
console.log("Tab Click:", cat.label);
state.selectedDepth1 = cat;
state.activeDepth2No = null;
renderTabs();
renderSidebar();
renderContent();
}
};
const handleSidebarClick = (no) => {
const section = document.getElementById(`section-${no}`);
if (section) {
console.log("Sidebar Click:", no);
isScrollingFromClick = true;
updateSidebarActive(no);
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Re-enable spy after transition
setTimeout(() => {
isScrollingFromClick = false;
}, 800);
}
};
const bindEvents = () => {
els.tabs?.addEventListener('click', e => {
const item = e.target.closest('.category-tab-item');
if (item) handleTabClick(Number(item.dataset.no));
});
els.sidebar?.addEventListener('click', e => {
const item = e.target.closest('.category-sidebar-item');
if (item) handleSidebarClick(Number(item.dataset.no));
});
};
const initialize = (categories) => {
console.log("Initializing Zigzag Layout. Count:", categories.length);
state.categories = categories;
if (state.categories.length > 0) {
state.selectedDepth1 = state.categories[0];
}
renderTabs(); renderSidebar(); renderContent(); bindEvents();
};
// --- Entry ---
EventManager.on('PAGE_LOAD_COMPLETED', (data) => {
if (data?.multiLevelCategories?.length > 0) {
initialize(data.multiLevelCategories);
} else {
fetchCategoriesFromApi().then(cats => {
if (cats.length > 0) initialize(cats);
});
}
});
setTimeout(() => {
if (state.categories.length === 0) {
fetchCategoriesFromApi().then(cats => {
if (cats.length > 0 && state.categories.length === 0) initialize(cats);
});
}
}, 500);
})();