/** * YS Project - Attendance Board Page JavaScript * Board-style listing of all active stores with latest attendance. * Same filter UI as landing page (PC custom dropdown + mobile native select). */ (function () { 'use strict'; // ============================================================ // State // ============================================================ var state = { page: 1, regionId: 0, industryId: 0, keyword: '', shuffleSeed: Math.floor(Math.random() * 2147483647), totalPages: 1, isLoading: false, filters: { regions: [], industries: [] }, }; // ============================================================ // DOM References // ============================================================ var $ = function (sel) { return document.querySelector(sel); }; var dom = { // Mobile native selects attRegion: $('#attRegion'), attSubRegion: $('#attSubRegion'), attIndustry: $('#attIndustry'), // PC custom dropdown triggers triggerRegion: $('#attTriggerRegion'), triggerSubRegion: $('#attTriggerSubRegion'), triggerIndustry: $('#attTriggerIndustry'), filtersDropdown: $('#attFiltersDropdown'), filtersDropdownGrid: $('#attFiltersDropdownGrid'), // Search attKeyword: $('#attKeyword'), attSearchBtn: $('#attSearchBtn'), // Board elements attPremiumBody: $('#attPremiumBody'), attNormalBody: $('#attNormalBody'), attBoardTable: $('#attBoardTable'), attLoading: $('#attLoading'), attEmpty: $('#attEmpty'), attPagination: $('#attPagination'), }; // ============================================================ // API Helper // ============================================================ async function fetchJSON(url) { var resp = await fetch(url); var data = await resp.json(); if (!resp.ok || !data.success) { throw new Error(data.error || 'Request failed'); } return data; } // ============================================================ // 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 */ }); } // ============================================================ // Load Filters // ============================================================ async function loadFilters() { try { var resp = await fetchJSON('/api/filters.php'); state.filters = resp.data; // Populate region dropdown state.filters.regions.forEach(function (r) { var opt = document.createElement('option'); opt.value = r.region_id; opt.textContent = r.region_name; dom.attRegion.appendChild(opt); }); // Populate industry dropdown state.filters.industries.forEach(function (ind) { var opt = document.createElement('option'); opt.value = ind.industry_id; opt.textContent = ind.industry_name; dom.attIndustry.appendChild(opt); }); } catch (err) { console.error('Failed to load filters:', err); } } function updateSubRegionDropdown(parentRegionId) { var subSelect = dom.attSubRegion; subSelect.innerHTML = ''; if (parentRegionId === 0) { subSelect.disabled = true; return; } var parent = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === parentRegionId; }); if (parent && parent.children && parent.children.length > 0) { parent.children.forEach(function (child) { var 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 // ============================================================ var 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.attRegion.value, 10); break; case 'subregion': items.push({ id: 0, name: '전체' }); var parentId = parseInt(dom.attRegion.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.attSubRegion.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.attIndustry.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.attRegion.value = numValue; updateSubRegionDropdown(numValue); state.regionId = numValue; state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); 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(); loadBoardFeed(); break; } case 'subregion': { dom.attSubRegion.value = numValue; if (numValue > 0) { state.regionId = numValue; } else { state.regionId = parseInt(dom.attRegion.value, 10); } state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); var subName = numValue === 0 ? '전체' : (function () { var pId = parseInt(dom.attRegion.value, 10); var parent = state.filters.regions.find(function (r) { return parseInt(r.region_id, 10) === pId; }); 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); loadBoardFeed(); break; } case 'industry': { dom.attIndustry.value = numValue; state.industryId = numValue; state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); 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); loadBoardFeed(); 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.attRegion.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 Feed // ============================================================ async function loadBoardFeed() { if (state.isLoading) return; state.isLoading = true; dom.attPremiumBody.innerHTML = ''; dom.attNormalBody.innerHTML = ''; dom.attEmpty.style.display = 'none'; dom.attPagination.style.display = 'none'; dom.attLoading.style.display = 'block'; var params = new URLSearchParams({ page: state.page, region_id: state.regionId, industry_id: state.industryId, keyword: state.keyword, seed: state.shuffleSeed, view_mode: 'list', search_desc: 1, }); try { var resp = await fetchJSON('/api/main_feed.php?' + params); var data = resp.data; // Render premium rows (page 1 only) if (data.premium_stores && data.premium_stores.length > 0 && state.page === 1) { renderBoardRows(data.premium_stores, dom.attPremiumBody, true); } // Render normal rows if (data.stores.length > 0) { renderBoardRows(data.stores, dom.attNormalBody, false); } // Empty state if (state.page === 1 && data.stores.length === 0 && (!data.premium_stores || data.premium_stores.length === 0)) { dom.attEmpty.style.display = 'block'; } // Pagination state.totalPages = data.pagination.total_pages; if (data.pagination.total_pages > 1) { renderPagination(data.pagination); dom.attPagination.style.display = 'flex'; } } catch (err) { if (state.page === 1) { dom.attEmpty.style.display = 'block'; } console.error('Failed to load attendance board:', err); } finally { state.isLoading = false; dom.attLoading.style.display = 'none'; } } // ============================================================ // Render Board Rows // ============================================================ 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; var regionText = store.region_display || store.sub_region_name || ''; var descText = store.description_text || ''; var ownerText = store.owner_display_name || store.owner_login_id || '-'; // Prefix: [region-storeName] var prefixHtml = ''; if (regionText) { prefixHtml = '[' + escapeHTML(regionText) + '-' + escapeHTML(store.store_name) + '] '; } else { prefixHtml = '[' + escapeHTML(store.store_name) + '] '; } var titleText = descText || store.store_name; tr.innerHTML = '' + '
' + prefixHtml + '' + escapeHTML(titleText) + '' + '
' + '' + '' + escapeHTML(ownerText) + ''; fragment.appendChild(tr); }); tbody.appendChild(fragment); } // ============================================================ // Pagination // ============================================================ function renderPagination(pagination) { var page = pagination.page; var totalPages = pagination.total_pages; if (totalPages <= 1) return; var container = dom.attPagination; container.innerHTML = ''; // Previous container.appendChild(createPageBtn('< 이전', page - 1, page <= 1)); var startPage = Math.max(1, page - 2); var endPage = Math.min(totalPages, page + 2); if (startPage > 1) { container.appendChild(createPageBtn('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 = createPageBtn(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(createPageBtn(String(totalPages), totalPages)); } // Next container.appendChild(createPageBtn('다음 >', page + 1, page >= totalPages)); } function createPageBtn(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; loadBoardFeed().then(function () { dom.attBoardTable.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); } return btn; } // ============================================================ // Row Click Handler // ============================================================ function handleRowClick(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=attendance'; } } // ============================================================ // Search // ============================================================ function doSearch() { state.keyword = dom.attKeyword.value.trim(); state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); loadBoardFeed(); } // ============================================================ // 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 // ============================================================ var toastTimeout = null; function showToast(msg) { var toast = document.querySelector('.toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('toast--visible'); if (toastTimeout) clearTimeout(toastTimeout); toastTimeout = setTimeout(function () { toast.classList.remove('toast--visible'); }, 2500); } // ============================================================ // Utility Functions // ============================================================ function escapeHTML(str) { if (!str) return ''; var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // ============================================================ // Event Bindings // ============================================================ function bindEvents() { // Mobile native select - Region dom.attRegion.addEventListener('change', function () { var parentId = parseInt(dom.attRegion.value, 10); updateSubRegionDropdown(parentId); state.regionId = parentId; state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); loadBoardFeed(); }); // Mobile native select - Sub-region dom.attSubRegion.addEventListener('change', function () { var subId = parseInt(dom.attSubRegion.value, 10); state.regionId = subId > 0 ? subId : parseInt(dom.attRegion.value, 10); state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); loadBoardFeed(); }); // Mobile native select - Industry dom.attIndustry.addEventListener('change', function () { state.industryId = parseInt(dom.attIndustry.value, 10); state.page = 1; state.shuffleSeed = Math.floor(Math.random() * 2147483647); loadBoardFeed(); }); // 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(); } } }); // Search button dom.attSearchBtn.addEventListener('click', doSearch); // Search on Enter dom.attKeyword.addEventListener('keydown', function (e) { if (e.key === 'Enter') doSearch(); }); // Row click (event delegation) dom.attPremiumBody.addEventListener('click', handleRowClick); dom.attNormalBody.addEventListener('click', handleRowClick); } // ============================================================ // Initialize // ============================================================ async function init() { bindEvents(); await loadFilters(); await loadBoardFeed(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();