/**
* 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')
? '✂
'
: '
';
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')
? `
✂
`
: `
})
`
}
${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();
}
})();