/** * YS Project - Main Application JavaScript * Mobile-first, vanilla JS (no framework dependency) */ (function () { 'use strict'; // ============================================================ // State // ============================================================ const state = { page: 1, regionId: 0, industryId: 0, keyword: '', shuffleSeed: Math.floor(Math.random() * 2147483647), totalPages: 1, isLoading: false, hasMore: true, filters: { regions: [], industries: [] }, viewMode: (window.YS && window.YS.initialViewMode) || localStorage.getItem('ys_view_mode') || 'card', // 'card' | 'list' }; // ============================================================ // DOM References // ============================================================ const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const dom = { searchKeyword: $('#searchKeyword'), searchBtn: $('#searchBtn'), filterRegion: $('#filterRegion'), filterSubRegion: $('#filterSubRegion'), filterIndustry: $('#filterIndustry'), // PC custom dropdown triggers triggerRegion: $('#triggerRegion'), triggerSubRegion: $('#triggerSubRegion'), triggerIndustry: $('#triggerIndustry'), filtersDropdown: $('#filtersDropdown'), filtersDropdownGrid: $('#filtersDropdownGrid'), // Card view premiumSection: $('#premiumSection'), premiumGrid: $('#premiumGrid'), storeGrid: $('#storeGrid'), storeLoading: $('#storeLoading'), storeEmpty: $('#storeEmpty'), storeSection: $('#storeSection'), // Infinite scroll elements infiniteLoader: $('#infiniteLoader'), infiniteEnd: $('#infiniteEnd'), infiniteSentinel: $('#infiniteSentinel'), // View toggle viewCardBtn: $('#viewCardBtn'), viewListBtn: $('#viewListBtn'), // List (board) view boardView: $('#boardView'), boardTable: $('#boardTable'), boardPremiumBody: $('#boardPremiumBody'), boardNormalBody: $('#boardNormalBody'), boardLoading: $('#boardLoading'), boardEmpty: $('#boardEmpty'), boardPagination: $('#boardPagination'), }; // ============================================================ // API Helpers // ============================================================ async function fetchJSON(url, options = {}) { try { const resp = await fetch(url, options); const data = await resp.json(); if (!resp.ok || !data.success) { throw new Error(data.error || 'Request failed'); } return data; } catch (err) { console.error('API Error:', err); throw err; } } // ============================================================ // View Count Increment (fire-and-forget) // ============================================================ function incrementViewCount(storeId) { fetch('/api/store_view.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ store_id: parseInt(storeId, 10) }), }).catch(function () { /* silent */ }); } // ============================================================ // View Mode Toggle // ============================================================ function setViewMode(mode) { state.viewMode = mode; localStorage.setItem('ys_view_mode', mode); // Save to DB for logged-in users (fire-and-forget) if (window.YS && window.YS.isLoggedIn) { var body = new URLSearchParams({ view_mode: mode, csrf_token: window.YS.csrfToken || '', }); fetch('/api/save_view_mode.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body, }).catch(function () { /* silent */ }); } // Update toggle buttons dom.viewCardBtn.classList.toggle('filters__view-btn--active', mode === 'card'); dom.viewListBtn.classList.toggle('filters__view-btn--active', mode === 'list'); // Toggle visibility if (mode === 'card') { dom.premiumSection.style.display = ''; dom.storeSection.style.display = ''; dom.infiniteSentinel.style.display = ''; dom.boardView.style.display = 'none'; } else { dom.premiumSection.style.display = 'none'; dom.storeSection.style.display = 'none'; dom.infiniteLoader.style.display = 'none'; dom.infiniteEnd.style.display = 'none'; dom.infiniteSentinel.style.display = 'none'; dom.boardView.style.display = ''; } // Reset and reload feed state.page = 1; state.hasMore = true; state.shuffleSeed = Math.floor(Math.random() * 2147483647); loadFeed(); } // ============================================================ // Load Filters (regions with children, industries) // ============================================================ async function loadFilters() { try { const resp = await fetchJSON('/api/filters.php'); state.filters = resp.data; // Populate parent region dropdown const regionSelect = dom.filterRegion; state.filters.regions.forEach((r) => { const opt = document.createElement('option'); opt.value = r.region_id; opt.textContent = r.region_name; regionSelect.appendChild(opt); }); // Populate industry dropdown const industrySelect = dom.filterIndustry; state.filters.industries.forEach((ind) => { const opt = document.createElement('option'); opt.value = ind.industry_id; opt.textContent = ind.industry_name; industrySelect.appendChild(opt); }); // Restore user filter var uf = resp.data.user_filter; if (uf) { restoreUserFilter(uf.region_id, uf.industry_id); } } catch (err) { console.error('Failed to load filters:', err); } } function restoreUserFilter(regionId, industryId) { if (regionId > 0) { var parentRegion = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === regionId; }); if (parentRegion) { dom.filterRegion.value = regionId; updateSubRegionDropdown(regionId); state.regionId = regionId; if (dom.triggerRegion) { dom.triggerRegion.textContent = parentRegion.region_name; updateTriggerActiveState(dom.triggerRegion, true); updateSubRegionTrigger(); } } else { for (var i = 0; i < state.filters.regions.length; i++) { var p = state.filters.regions[i]; if (p.children) { var child = p.children.find(function (c) { return parseInt(c.region_id, 10) === regionId; }); if (child) { var parentId = parseInt(p.region_id, 10); dom.filterRegion.value = parentId; updateSubRegionDropdown(parentId); dom.filterSubRegion.value = regionId; state.regionId = regionId; if (dom.triggerRegion) { dom.triggerRegion.textContent = p.region_name; updateTriggerActiveState(dom.triggerRegion, true); updateSubRegionTrigger(); dom.triggerSubRegion.textContent = child.region_name; updateTriggerActiveState(dom.triggerSubRegion, true); } break; } } } } } if (industryId > 0) { dom.filterIndustry.value = industryId; state.industryId = industryId; if (dom.triggerIndustry) { var ind = state.filters.industries.find(function (i) { return parseInt(i.industry_id, 10) === industryId; }); if (ind) { dom.triggerIndustry.textContent = ind.industry_name; updateTriggerActiveState(dom.triggerIndustry, true); } } } } var _saveFilterTimer = null; function saveFilterToServer() { if (!window.YS || !window.YS.isLoggedIn) return; clearTimeout(_saveFilterTimer); _saveFilterTimer = setTimeout(function () { var body = new URLSearchParams({ region_id: state.regionId, industry_id: state.industryId, csrf_token: window.YS.csrfToken || '', }); fetch('/api/save_filter.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body, }).catch(function () { /* silent */ }); }, 1000); } function updateSubRegionDropdown(parentRegionId) { const subSelect = dom.filterSubRegion; subSelect.innerHTML = ''; if (parentRegionId === 0) { subSelect.disabled = true; return; } const parent = state.filters.regions.find( (r) => parseInt(r.region_id, 10) === parentRegionId ); if (parent && parent.children && parent.children.length > 0) { parent.children.forEach((child) => { const opt = document.createElement('option'); opt.value = child.region_id; opt.textContent = child.region_name; subSelect.appendChild(opt); }); subSelect.disabled = false; } else { subSelect.disabled = true; } } // ============================================================ // PC Custom Dropdown // ============================================================ let activeDropdown = null; function isPCMode() { return window.innerWidth >= 768; } function getOptimalColumns(count) { if (count <= 8) return count; for (var cols = 8; cols >= 4; cols--) { if (count % cols === 0) return cols; } var bestCols = 8; var minEmpty = count; for (var cols = 8; cols >= 4; cols--) { var remainder = count % cols; var empty = remainder === 0 ? 0 : cols - remainder; if (empty < minEmpty) { minEmpty = empty; bestCols = cols; } } return bestCols; } function openFilterDropdown(type) { if (!isPCMode()) return; if (activeDropdown === type) { closeFilterDropdown(); return; } activeDropdown = type; var triggers = [dom.triggerRegion, dom.triggerSubRegion, dom.triggerIndustry]; triggers.forEach(function (t) { if (t) t.classList.remove('filters__trigger--open'); }); var triggerMap = { region: dom.triggerRegion, subregion: dom.triggerSubRegion, industry: dom.triggerIndustry }; if (triggerMap[type]) triggerMap[type].classList.add('filters__trigger--open'); var items = []; var selectedValue = 0; switch (type) { case 'region': items.push({ id: 0, name: '전체 지역' }); state.filters.regions.forEach(function (r) { items.push({ id: parseInt(r.region_id, 10), name: r.region_name }); }); selectedValue = parseInt(dom.filterRegion.value, 10); break; case 'subregion': items.push({ id: 0, name: '전체' }); var parentId = parseInt(dom.filterRegion.value, 10); var parentRegion = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === parentId; }); if (parentRegion && parentRegion.children) { parentRegion.children.forEach(function (c) { items.push({ id: parseInt(c.region_id, 10), name: c.region_name }); }); } selectedValue = parseInt(dom.filterSubRegion.value, 10); break; case 'industry': items.push({ id: 0, name: '전체 업종' }); state.filters.industries.forEach(function (ind) { items.push({ id: parseInt(ind.industry_id, 10), name: ind.industry_name }); }); selectedValue = parseInt(dom.filterIndustry.value, 10); break; } var cols = getOptimalColumns(items.length); dom.filtersDropdownGrid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)'; dom.filtersDropdownGrid.innerHTML = items.map(function (item) { var isSelected = item.id === selectedValue; return '
' + escapeHTML(item.name) + '
'; }).join(''); dom.filtersDropdown.classList.add('filters__dropdown--open'); } function closeFilterDropdown() { activeDropdown = null; dom.filtersDropdown.classList.remove('filters__dropdown--open'); [dom.triggerRegion, dom.triggerSubRegion, dom.triggerIndustry].forEach(function (t) { if (t) t.classList.remove('filters__trigger--open'); }); } function handleDropdownItemClick(value) { var numValue = parseInt(value, 10); var type = activeDropdown; switch (type) { case 'region': { dom.filterRegion.value = numValue; updateSubRegionDropdown(numValue); state.regionId = numValue; state.page = 1; var regionName = numValue === 0 ? '전체 지역' : (function () { var r = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === numValue; }); return r ? r.region_name : '전체 지역'; })(); dom.triggerRegion.textContent = regionName; updateTriggerActiveState(dom.triggerRegion, numValue !== 0); updateSubRegionTrigger(); loadFeed(); saveFilterToServer(); break; } case 'subregion': { dom.filterSubRegion.value = numValue; if (numValue > 0) { state.regionId = numValue; } else { state.regionId = parseInt(dom.filterRegion.value, 10); } state.page = 1; var subName = numValue === 0 ? '전체' : (function () { var parentId = parseInt(dom.filterRegion.value, 10); var parent = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === parentId; }); var child = parent && parent.children ? parent.children.find(function (c) { return parseInt(c.region_id, 10) === numValue; }) : null; return child ? child.region_name : '전체'; })(); dom.triggerSubRegion.textContent = subName; updateTriggerActiveState(dom.triggerSubRegion, numValue !== 0); loadFeed(); saveFilterToServer(); break; } case 'industry': { dom.filterIndustry.value = numValue; state.industryId = numValue; state.page = 1; var indName = numValue === 0 ? '전체 업종' : (function () { var ind = state.filters.industries.find(function (i) { return parseInt(i.industry_id, 10) === numValue; }); return ind ? ind.industry_name : '전체 업종'; })(); dom.triggerIndustry.textContent = indName; updateTriggerActiveState(dom.triggerIndustry, numValue !== 0); loadFeed(); saveFilterToServer(); break; } } closeFilterDropdown(); } function updateTriggerActiveState(trigger, isActive) { if (!trigger) return; if (isActive) { trigger.classList.add('filters__trigger--active'); } else { trigger.classList.remove('filters__trigger--active'); } } function updateSubRegionTrigger() { if (!dom.triggerSubRegion) return; var parentId = parseInt(dom.filterRegion.value, 10); var parent = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === parentId; }); var hasChildren = parent && parent.children && parent.children.length > 0; if (parentId === 0 || !hasChildren) { dom.triggerSubRegion.classList.add('filters__trigger--disabled'); dom.triggerSubRegion.disabled = true; dom.triggerSubRegion.textContent = '전체'; dom.triggerSubRegion.classList.remove('filters__trigger--active'); } else { dom.triggerSubRegion.classList.remove('filters__trigger--disabled'); dom.triggerSubRegion.disabled = false; dom.triggerSubRegion.textContent = '전체'; dom.triggerSubRegion.classList.remove('filters__trigger--active'); } } // ============================================================ // Load Main Feed (supports infinite scroll for card, pagination for list) // ============================================================ async function resetAndLoadFeed() { state.page = 1; state.hasMore = true; state.shuffleSeed = Math.floor(Math.random() * 2147483647); state.isLoading = false; if (state.viewMode === 'card') { // Card view reset dom.premiumGrid.innerHTML = ''; dom.premiumSection.style.display = 'none'; dom.storeGrid.innerHTML = ''; dom.storeEmpty.style.display = 'none'; dom.infiniteEnd.style.display = 'none'; dom.storeLoading.style.display = 'block'; await loadFeedPage(); dom.storeLoading.style.display = 'none'; } else { // List view reset dom.boardPremiumBody.innerHTML = ''; dom.boardNormalBody.innerHTML = ''; dom.boardEmpty.style.display = 'none'; dom.boardPagination.style.display = 'none'; dom.boardLoading.style.display = 'block'; await loadListFeed(); dom.boardLoading.style.display = 'none'; } } /** * Load card view page (infinite scroll) */ async function loadFeedPage() { if (state.isLoading || !state.hasMore) return; state.isLoading = true; if (state.page > 1) { dom.infiniteLoader.style.display = 'block'; } const params = new URLSearchParams({ page: state.page, region_id: state.regionId, industry_id: state.industryId, keyword: state.keyword, seed: state.shuffleSeed, view_mode: 'card', }); try { const resp = await fetchJSON('/api/main_feed.php?' + params); const { premium_stores, stores, pagination } = resp.data; if (premium_stores && premium_stores.length > 0 && state.page === 1) { renderPremiumStores(premium_stores); dom.premiumSection.style.display = ''; } if (stores.length > 0) { renderStores(stores); } if (state.page === 1 && stores.length === 0 && (!premium_stores || premium_stores.length === 0)) { dom.storeEmpty.style.display = 'block'; } state.totalPages = pagination.total_pages; if (state.page >= pagination.total_pages) { state.hasMore = false; dom.infiniteEnd.style.display = (state.page > 1) ? 'block' : 'none'; } } catch (err) { if (state.page === 1) { dom.storeEmpty.style.display = 'block'; } console.error('Failed to load feed:', err); } finally { state.isLoading = false; dom.infiniteLoader.style.display = 'none'; } } /** * Load list (board) view */ async function loadListFeed() { if (state.isLoading) return; state.isLoading = true; const params = new URLSearchParams({ page: state.page, region_id: state.regionId, industry_id: state.industryId, keyword: state.keyword, seed: state.shuffleSeed, view_mode: 'list', }); try { const resp = await fetchJSON('/api/main_feed.php?' + params); const { premium_stores, stores, pagination } = resp.data; // Render premium rows (page 1 only, show all premium) if (premium_stores && premium_stores.length > 0 && state.page === 1) { renderBoardRows(premium_stores, dom.boardPremiumBody, true); } // Render normal rows if (stores.length > 0) { renderBoardRows(stores, dom.boardNormalBody, false); } // Empty state if (state.page === 1 && stores.length === 0 && (!premium_stores || premium_stores.length === 0)) { dom.boardEmpty.style.display = 'block'; } // Pagination state.totalPages = pagination.total_pages; if (pagination.total_pages > 1) { renderBoardPagination(pagination); dom.boardPagination.style.display = 'flex'; } } catch (err) { if (state.page === 1) { dom.boardEmpty.style.display = 'block'; } console.error('Failed to load list feed:', err); } finally { state.isLoading = false; } } /** * Backward-compatible wrapper */ async function loadFeed() { await resetAndLoadFeed(); } // ============================================================ // Render Premium Store Cards (Card View) // ============================================================ function renderPremiumStores(stores) { var count = stores.length; var layoutClass = count >= 4 ? 'premium-grid--quad' : count === 3 ? 'premium-grid--trio' : count === 2 ? 'premium-grid--duo' : 'premium-grid--solo'; dom.premiumGrid.className = 'premium-grid ' + layoutClass; var fragment = document.createDocumentFragment(); var displayStores = count >= 4 ? stores.slice(0, 4) : stores; displayStores.forEach(function (store) { var card = document.createElement('article'); card.className = 'premium-card'; card.dataset.storeId = store.store_id; var heartClass = store.is_hearted ? 'premium-card__heart--active' : 'premium-card__heart--inactive'; var heartSymbol = store.is_hearted ? '♥' : '♡'; var imageHTML = store.image_url.endsWith('.svg') ? '
' : '' + escapeAttr(store.store_name) + ''; var infoItems = []; if (store.business_hours) infoItems.push('🕔 ' + escapeHTML(store.business_hours) + ''); if (store.address) infoItems.push('📍 ' + escapeHTML(store.address) + ''); card.innerHTML = '
' + 'PREMIUM' + '' + imageHTML + '
' + '
' + '

' + escapeHTML(store.store_name) + '

' + '
' + (store.region_name ? '' + escapeHTML(store.region_name) + '' : '') + (store.industry_name ? '' + escapeHTML(store.industry_name) + '' : '') + '
' + (infoItems.length > 0 ? '
' + infoItems.join('') + '
' : '') + (store.description ? '

' + escapeHTML(store.description) + '

' : '') + '
' + ' ' + escapeHTML(store.avg_rating) + '' + ' ' + formatCount(store.review_count) + '' + ' ' + formatCount(store.heart_count) + '' + '
' + '
'; fragment.appendChild(card); }); dom.premiumGrid.appendChild(fragment); } // ============================================================ // Render Store Cards (Card View) // ============================================================ function renderStores(stores) { const fragment = document.createDocumentFragment(); stores.forEach((store) => { const card = document.createElement('article'); card.className = `store-card${store.ad_plan === 'premium' ? ' store-card--premium' : ''}`; card.dataset.storeId = store.store_id; const heartClass = store.is_hearted ? 'store-card__heart--active' : 'store-card__heart--inactive'; const heartSymbol = store.is_hearted ? '♥' : '♡'; card.innerHTML = `
${store.ad_plan === 'premium' ? 'PREMIUM' : ''} ${store.image_url.endsWith('.svg') ? `
` : `${escapeAttr(store.store_name)}` }

${escapeHTML(store.store_name)}

${store.region_name ? `${escapeHTML(store.region_name)}` : ''} ${store.industry_name ? `${escapeHTML(store.industry_name)}` : ''}
${escapeHTML(store.avg_rating)} ${formatCount(store.review_count)} ${formatCount(store.heart_count)}
`; fragment.appendChild(card); }); dom.storeGrid.appendChild(fragment); } // ============================================================ // Render Board Rows (List View) // ============================================================ function renderBoardRows(stores, tbody, isPremium) { var fragment = document.createDocumentFragment(); stores.forEach(function (store) { var tr = document.createElement('tr'); tr.className = 'board-table__row' + (isPremium ? ' board-table__row--premium' : ''); tr.dataset.storeId = store.store_id; // Region display for prefix tag var regionText = store.region_display || store.sub_region_name || ''; // Description as title var descText = store.description_text || ''; // Owner display name (nickname preferred) var ownerText = store.owner_display_name || store.owner_login_id || '-'; // Build prefix: [region] storeName (like community board store-tag) var prefixHtml = ''; if (regionText) { prefixHtml = '[' + escapeHTML(regionText) + '-' + escapeHTML(store.store_name) + '] '; } else { prefixHtml = '[' + escapeHTML(store.store_name) + '] '; } // Title text: description or store name fallback var titleText = descText || store.store_name; tr.innerHTML = '' + '
' + prefixHtml + '' + escapeHTML(titleText) + '' + '
' + '' + '' + escapeHTML(ownerText) + ''; fragment.appendChild(tr); }); tbody.appendChild(fragment); } // ============================================================ // Board Pagination // ============================================================ function renderBoardPagination(pagination) { var page = pagination.page; var totalPages = pagination.total_pages; if (totalPages <= 1) return; var container = dom.boardPagination; container.innerHTML = ''; // Previous var prevBtn = createBoardPageBtn('< 이전', page - 1, page <= 1); container.appendChild(prevBtn); var startPage = Math.max(1, page - 2); var endPage = Math.min(totalPages, page + 2); if (startPage > 1) { container.appendChild(createBoardPageBtn('1', 1)); if (startPage > 2) { var dots = document.createElement('span'); dots.className = 'board-pagination__btn board-pagination__btn--dots'; dots.textContent = '...'; container.appendChild(dots); } } for (var i = startPage; i <= endPage; i++) { var btn = createBoardPageBtn(String(i), i); if (i === page) btn.classList.add('board-pagination__btn--active'); container.appendChild(btn); } if (endPage < totalPages) { if (endPage < totalPages - 1) { var dots2 = document.createElement('span'); dots2.className = 'board-pagination__btn board-pagination__btn--dots'; dots2.textContent = '...'; container.appendChild(dots2); } container.appendChild(createBoardPageBtn(String(totalPages), totalPages)); } // Next var nextBtn = createBoardPageBtn('다음 >', page + 1, page >= totalPages); container.appendChild(nextBtn); } function createBoardPageBtn(label, page, disabled) { var btn = document.createElement('button'); btn.className = 'board-pagination__btn'; btn.textContent = label; btn.disabled = !!disabled; if (!disabled) { btn.addEventListener('click', function () { state.page = page; // Clear and reload list view dom.boardNormalBody.innerHTML = ''; dom.boardPremiumBody.innerHTML = ''; dom.boardPagination.style.display = 'none'; dom.boardLoading.style.display = 'block'; loadListFeed().then(function () { dom.boardLoading.style.display = 'none'; // Scroll to top of board dom.boardView.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); } return btn; } // ============================================================ // Infinite Scroll (IntersectionObserver) - Card View Only // ============================================================ let scrollObserver = null; function setupInfiniteScroll() { if (!dom.infiniteSentinel) return; scrollObserver = new IntersectionObserver(function (entries) { var entry = entries[0]; if (entry.isIntersecting && !state.isLoading && state.hasMore && state.viewMode === 'card') { state.page++; loadFeedPage(); } }, { root: null, rootMargin: '0px 0px 300px 0px', threshold: 0 }); scrollObserver.observe(dom.infiniteSentinel); } // ============================================================ // Heart Toggle // ============================================================ async function toggleHeart(storeId, btnElement) { if (!window.YS.isLoggedIn) { showToast(window.YS.isOwner ? '일반 회원으로 로그인 후 이용해 주세요.' : '로그인 후 이용해 주세요.'); return; } try { const resp = await fetchJSON('/api/heart_toggle.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ store_id: parseInt(storeId, 10) }), }); const { is_hearted, heart_count } = resp.data; btnElement.innerHTML = is_hearted ? '♥' : '♡'; var isPremium = btnElement.classList.contains('premium-card__heart') || btnElement.classList.contains('premium-card__heart--active') || btnElement.classList.contains('premium-card__heart--inactive'); var prefix = isPremium ? 'premium-card__heart' : 'store-card__heart'; btnElement.className = prefix + ' ' + (is_hearted ? prefix + '--active' : prefix + '--inactive'); const countEl = document.querySelector('.heart-count-' + storeId); if (countEl) { countEl.textContent = formatCount(heart_count); } showToast(is_hearted ? '즐겨찾기에 추가했습니다.' : '즐겨찾기를 해제했습니다.'); } catch (err) { showToast('처리 중 오류가 발생했습니다.'); } } // ============================================================ // Event Handlers // ============================================================ function bindEvents() { // Search dom.searchBtn.addEventListener('click', doSearch); dom.searchKeyword.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); }); // Filters - Region (parent) dom.filterRegion.addEventListener('change', () => { const parentId = parseInt(dom.filterRegion.value, 10); updateSubRegionDropdown(parentId); state.regionId = parentId; state.page = 1; loadFeed(); saveFilterToServer(); }); // Filters - Sub-region (child) dom.filterSubRegion.addEventListener('change', () => { const subId = parseInt(dom.filterSubRegion.value, 10); if (subId > 0) { state.regionId = subId; } else { state.regionId = parseInt(dom.filterRegion.value, 10); } state.page = 1; loadFeed(); saveFilterToServer(); }); dom.filterIndustry.addEventListener('change', () => { state.industryId = parseInt(dom.filterIndustry.value, 10); state.page = 1; loadFeed(); saveFilterToServer(); }); // PC Custom Dropdown - trigger clicks if (dom.triggerRegion) { dom.triggerRegion.addEventListener('click', function () { openFilterDropdown('region'); }); } if (dom.triggerSubRegion) { dom.triggerSubRegion.addEventListener('click', function () { openFilterDropdown('subregion'); }); } if (dom.triggerIndustry) { dom.triggerIndustry.addEventListener('click', function () { openFilterDropdown('industry'); }); } // PC Custom Dropdown - item click if (dom.filtersDropdownGrid) { dom.filtersDropdownGrid.addEventListener('click', function (e) { var item = e.target.closest('.filters__dropdown-item'); if (item) { handleDropdownItemClick(item.dataset.value); } }); } // Click outside to close dropdown document.addEventListener('click', function (e) { if (activeDropdown && dom.filtersDropdown) { var isInsideDropdown = dom.filtersDropdown.contains(e.target); var isTrigger = e.target.closest('.filters__trigger'); if (!isInsideDropdown && !isTrigger) { closeFilterDropdown(); } } }); // Heart toggle (event delegation) - Premium grid dom.premiumGrid.addEventListener('click', function (e) { var heartBtn = e.target.closest('.premium-card__heart'); if (heartBtn) { e.preventDefault(); e.stopPropagation(); toggleHeart(heartBtn.dataset.storeId, heartBtn); return; } var card = e.target.closest('.premium-card'); if (card && card.dataset.storeId) { if (!checkStoreViewPermission()) return; incrementViewCount(card.dataset.storeId); window.location.href = '/store.php?id=' + card.dataset.storeId; } }); // Heart toggle (event delegation) - Store grid dom.storeGrid.addEventListener('click', (e) => { const heartBtn = e.target.closest('.store-card__heart'); if (heartBtn) { e.preventDefault(); e.stopPropagation(); const storeId = heartBtn.dataset.storeId; toggleHeart(storeId, heartBtn); return; } const card = e.target.closest('.store-card'); if (card) { const storeId = card.dataset.storeId; if (storeId) { if (!checkStoreViewPermission()) return; incrementViewCount(storeId); window.location.href = '/store.php?id=' + storeId; } } }); // View mode toggle if (dom.viewCardBtn) { dom.viewCardBtn.addEventListener('click', function () { if (state.viewMode !== 'card') setViewMode('card'); }); } if (dom.viewListBtn) { dom.viewListBtn.addEventListener('click', function () { if (state.viewMode !== 'list') setViewMode('list'); }); } // Board view row click (event delegation) if (dom.boardPremiumBody) { dom.boardPremiumBody.addEventListener('click', handleBoardRowClick); } if (dom.boardNormalBody) { dom.boardNormalBody.addEventListener('click', handleBoardRowClick); } } function handleBoardRowClick(e) { var row = e.target.closest('.board-table__row'); if (row && row.dataset.storeId) { if (!checkStoreViewPermission()) return; incrementViewCount(row.dataset.storeId); window.location.href = '/store_board.php?id=' + row.dataset.storeId + '&from=main'; } } function doSearch() { state.keyword = dom.searchKeyword.value.trim(); state.page = 1; loadFeed(); } // ============================================================ // Store View Permission Check // ============================================================ function checkStoreViewPermission() { var ys = window.YS || {}; var minLevel = parseInt(ys.storeViewMinLevel, 10) || 0; if (minLevel <= 0) return true; if (!ys.isLoggedIn) { showToast('로그인이 필요합니다.'); return false; } var userLevel = parseInt(ys.userLevel, 10) || 0; if (userLevel < minLevel) { showToast('레벨 ' + minLevel + ' 이상부터 매장 정보를 열람할 수 있습니다.'); return false; } return true; } // ============================================================ // Toast Notification // ============================================================ let toastTimeout = null; function showToast(message) { let toast = document.querySelector('.toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'toast'; document.body.appendChild(toast); } toast.textContent = message; toast.classList.add('toast--visible'); if (toastTimeout) clearTimeout(toastTimeout); toastTimeout = setTimeout(() => { toast.classList.remove('toast--visible'); }, 2500); } // ============================================================ // Utility Functions // ============================================================ function escapeHTML(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function escapeAttr(str) { if (!str) return ''; return str.replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } function formatCount(num) { num = parseInt(num, 10) || 0; if (num >= 10000) return (num / 10000).toFixed(1) + '\uB9CC'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return String(num); } // ============================================================ // Initialize // ============================================================ async function init() { // Apply saved view mode on load if (state.viewMode === 'list') { dom.viewCardBtn.classList.remove('filters__view-btn--active'); dom.viewListBtn.classList.add('filters__view-btn--active'); dom.premiumSection.style.display = 'none'; dom.storeSection.style.display = 'none'; dom.infiniteSentinel.style.display = 'none'; dom.boardView.style.display = ''; } bindEvents(); await loadFilters(); await loadFeed(); setupInfiniteScroll(); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();