/**
* 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();
}
})();