/** * YS Project - Store Detail Page JavaScript * Mobile-first, vanilla JS (no framework dependency) */ (function () { 'use strict'; // ============================================================ // State // ============================================================ const state = { storeId: window.YS.storeId, store: null, isHearted: false, heartCount: 0, reviewPage: 1, reviewTotalPages: 1, userReviewId: null, selectedRating: 0, reportingReviewId: null, isSubmitting: false, }; // ============================================================ // DOM Helpers // ============================================================ const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const dom = { storeLoading: $('#storeLoading'), storeError: $('#storeError'), storeErrorText: $('#storeErrorText'), storeContent: $('#storeContent'), storeImageWrap: $('#storeImageWrap'), storeBadge: $('#storeBadge'), storeName: $('#storeName'), storeMeta: $('#storeMeta'), storeRating: $('#storeRating'), storeDescription: $('#storeDescription'), storeActions: $('#storeActions'), storeDetails: $('#storeDetails'), storeTable: $('#storeTable'), tabsSection: $('#tabsSection'), tabAtt: $('#tabAtt'), tabPromo: $('#tabPromo'), promoList: $('#promoList'), promoEmpty: $('#promoEmpty'), attList: $('#attList'), attEmpty: $('#attEmpty'), reviewCountBadge: $('#reviewCountBadge'), reviewFormWrap: $('#reviewFormWrap'), reviewForm: $('#reviewForm'), alreadyReviewed: $('#alreadyReviewed'), loginPrompt: $('#loginPrompt'), reviewList: $('#reviewList'), reviewPagination: $('#reviewPagination'), reviewLoading: $('#reviewLoading'), noReviews: $('#noReviews'), starSelector: $('#starSelector'), ratingValue: $('#ratingValue'), reviewContent: $('#reviewContent'), reviewPhoto: $('#reviewPhoto'), photoFileName: $('#photoFileName'), reviewSubmitBtn: $('#reviewSubmitBtn'), reviewFormError: $('#reviewFormError'), reportModal: $('#reportModal'), reportModalClose: $('#reportModalClose'), reportCancelBtn: $('#reportCancelBtn'), reportSubmitBtn: $('#reportSubmitBtn'), }; // ============================================================ // API Helpers // ============================================================ async function fetchJSON(url, options = {}) { try { const resp = await fetch(url, options); const data = await resp.json(); if (!resp.ok || !data.success) { var err = new Error(data.error || 'Request failed'); err.status = resp.status; throw err; } return data; } catch (err) { console.error('API Error:', err); throw err; } } // ============================================================ // 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); } function renderStars(rating, size) { const full = Math.floor(rating); const half = rating - full >= 0.5 ? 1 : 0; const empty = 5 - full - half; let html = ''; for (let i = 0; i < full; i++) html += ''; if (half) html += ''; for (let i = 0; i < empty; i++) html += ''; return html; } // ============================================================ // 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); } // ============================================================ // Load Store Detail // ============================================================ async function loadStoreDetail() { dom.storeLoading.style.display = 'block'; dom.storeError.style.display = 'none'; dom.storeContent.style.display = 'none'; try { const resp = await fetchJSON('/api/store_detail.php?id=' + state.storeId); const { store, promotions, attendances, review_summary, is_hearted } = resp.data; state.store = store; state.isHearted = is_hearted; state.heartCount = parseInt(store.heart_count, 10) || 0; dom.storeLoading.style.display = 'none'; dom.storeContent.style.display = 'block'; // SSR(서버 렌더) 블록은 인터랙티브 UI로 대체되었으므로 숨김 (중복 h1 방지) var ssrSeo = document.getElementById('ssrSeo'); if (ssrSeo) ssrSeo.style.display = 'none'; // Update page title document.title = store.store_name + ' - ' + document.title.split(' - ').pop(); renderStoreHeader(store); renderActions(store); renderDetails(store); renderPromotions(promotions); renderAttendances(attendances); dom.tabsSection.style.display = 'block'; // Show review board links var reviewLinksEl = document.getElementById('reviewLinks'); if (reviewLinksEl) reviewLinksEl.style.display = ''; renderReviewSummary(review_summary); // Show review form or prompt setupReviewSection(); // Load reviews loadReviews(); } catch (err) { dom.storeLoading.style.display = 'none'; dom.storeError.style.display = 'block'; // 권한 부족/오류 시 SSR 블록도 숨겨 기존 UX(에러만 표시) 유지 var ssrSeoErr = document.getElementById('ssrSeo'); if (ssrSeoErr) ssrSeoErr.style.display = 'none'; if (err.status === 401) { dom.storeErrorText.textContent = err.message || '로그인 후 이용 가능합니다.'; } else if (err.status === 403) { dom.storeErrorText.textContent = err.message || '열람 권한이 없습니다.'; } else { dom.storeErrorText.textContent = err.message || '매장을 찾을 수 없습니다.'; } } } function showToast(msg) { var t = document.createElement('div'); t.textContent = msg; t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:10px 20px;border-radius:6px;font-size:14px;z-index:99999;opacity:0;transition:opacity .3s;'; document.body.appendChild(t); requestAnimationFrame(function(){ t.style.opacity = '1'; }); setTimeout(function(){ t.style.opacity = '0'; setTimeout(function(){ t.remove(); }, 300); }, 2500); } // ============================================================ // Render Store Header // ============================================================ function renderStoreHeader(store) { // Image if (store.image_url && !store.image_url.endsWith('.svg')) { dom.storeImageWrap.innerHTML = '' + escapeAttr(store.store_name) + ''; } else { dom.storeImageWrap.innerHTML = '
'; } // Badge if (store.ad_plan === 'premium') { dom.storeBadge.textContent = 'PREMIUM'; dom.storeBadge.className = 'sd__badge sd__badge--premium'; dom.storeBadge.style.display = 'inline-block'; } // Name dom.storeName.textContent = store.store_name; // Meta let metaHTML = ''; if (store.full_region_name) { metaHTML += '📍 ' + escapeHTML(store.full_region_name) + ''; } if (store.industry_name) { metaHTML += '💈 ' + escapeHTML(store.industry_name) + ''; } dom.storeMeta.innerHTML = metaHTML; // Rating const avgRating = parseFloat(store.avg_rating) || 0; const reviewCount = parseInt(store.review_count, 10) || 0; dom.storeRating.innerHTML = '
' + renderStars(avgRating, 'header') + '
' + '' + store.avg_rating + '' + '(' + formatCount(reviewCount) + '개 리뷰)'; // Description (server sanitizes to only allow and
) if (store.description) { dom.storeDescription.innerHTML = store.description; dom.storeDescription.style.display = 'block'; } } // ============================================================ // Render Action Buttons // ============================================================ function renderActions(store) { let html = ''; // Heart button const heartClass = state.isHearted ? ' active' : ''; const heartIcon = state.isHearted ? '♥' : '♡'; html += ''; // Phone if (store.phone_number) { html += '' + '📞' + '전화하기' + ''; } // Kakao if (store.messenger_kakao) { html += '' + '💬' + '카카오톡' + ''; } // Telegram if (store.messenger_telegram) { html += '' + '' + '텔레그램' + ''; } // WhatsApp if (store.messenger_whatsapp) { html += '' + '💬' + '왓츠앱' + ''; } // Message button (logged-in users only, not owners, min level check) if (window.YS.isLoggedIn && !window.YS.isOwner) { var minLv = window.YS.messageMinLevel || 5; if ((window.YS.userLevel || 0) >= minLv) { html += '' + '' + '메시지 보내기' + ''; } else { html += '' + '' + '메시지 (Lv.' + minLv + ')' + ''; } } dom.storeActions.innerHTML = html; // Bind heart button var heartBtn = document.getElementById('heartBtn'); if (heartBtn) { heartBtn.addEventListener('click', toggleHeart); } } // ============================================================ // Render Store Details Table // ============================================================ function renderDetails(store) { let rows = ''; if (store.phone_number) { rows += '전화번호' + escapeHTML(store.phone_number) + ''; } if (store.address) { rows += '주소' + escapeHTML(store.address) + ''; } if (store.business_hours) { rows += '영업시간' + escapeHTML(store.business_hours) + ''; } if (store.full_region_name) { rows += '지역' + escapeHTML(store.full_region_name) + ''; } if (store.industry_name) { rows += '업종' + escapeHTML(store.industry_name) + ''; } if (rows) { dom.storeTable.innerHTML = rows; dom.storeDetails.style.display = 'block'; } } // ============================================================ // Render Promotions // ============================================================ function renderPromotions(promotions) { if (!promotions || promotions.length === 0) { dom.promoEmpty.style.display = 'block'; return; } dom.promoEmpty.style.display = 'none'; let html = ''; // Show first 3 promotions, rest toggleable promotions.forEach(function (promo, idx) { var hiddenClass = idx >= 3 ? ' style="display:none;" data-extra-promo="true"' : ''; html += '
' + '

' + escapeHTML(promo.title) + '

' + '
' + promo.content + '
' + '
' + escapeHTML(promo.created_at ? promo.created_at.substring(0, 10) : '') + '
' + '
'; }); if (promotions.length > 3) { html += ''; } dom.promoList.innerHTML = html; // Toggle handler var toggle = document.getElementById('promoToggle'); if (toggle) { toggle.addEventListener('click', function () { var extras = dom.promoList.querySelectorAll('[data-extra-promo]'); var hidden = extras[0] && extras[0].style.display === 'none'; extras.forEach(function (el) { el.style.display = hidden ? 'block' : 'none'; }); toggle.textContent = hidden ? '접기' : '더보기 (' + extras.length + '개)'; }); } } // ============================================================ // Render Attendances // ============================================================ function renderAttendances(attendances) { if (!attendances || attendances.length === 0) { dom.attEmpty.style.display = 'block'; return; } dom.attEmpty.style.display = 'none'; let html = ''; var store = state.store || {}; var descText = (store.description || '').replace(/<[^>]*>/g, '').trim(); attendances.forEach(function (att) { // 표시 제목: title → description → store_name var displayTitle = (att.title && att.title.trim()) ? att.title : (descText || store.store_name || ''); var titleHtml = displayTitle ? '
' + escapeHTML(displayTitle) + '
' : ''; html += '
' + '
' + escapeHTML(att.date || '') + '
' + titleHtml + '
' + att.content + '
' + '
'; }); dom.attList.innerHTML = html; } // ============================================================ // Render Review Summary // ============================================================ function renderReviewSummary(summary) { var count = summary.review_count || 0; dom.reviewCountBadge.textContent = count > 0 ? '(' + formatCount(count) + ')' : ''; } // ============================================================ // Setup Review Section (form visibility) // ============================================================ function setupReviewSection() { if (window.YS.isLoggedIn && !window.YS.isOwner) { // Will show form or already-reviewed after reviews load dom.reviewFormWrap.style.display = 'block'; } else if (!window.YS.isLoggedIn) { dom.loginPrompt.style.display = 'block'; } // Owner: show nothing (owners don't review) } // ============================================================ // Load Reviews // ============================================================ async function loadReviews() { dom.reviewLoading.style.display = 'block'; dom.reviewList.innerHTML = ''; dom.reviewPagination.innerHTML = ''; dom.noReviews.style.display = 'none'; try { var params = new URLSearchParams({ store_id: state.storeId, page: state.reviewPage, per_page: 10, }); var resp = await fetchJSON('/api/reviews/list.php?' + params); var data = resp.data; dom.reviewLoading.style.display = 'none'; state.userReviewId = data.user_review_id; state.reviewTotalPages = data.pagination.total_pages; // Show/hide form based on whether user already reviewed if (window.YS.isLoggedIn && !window.YS.isOwner) { if (state.userReviewId) { dom.reviewFormWrap.style.display = 'none'; dom.alreadyReviewed.style.display = 'block'; } else { dom.reviewFormWrap.style.display = 'block'; dom.alreadyReviewed.style.display = 'none'; } } if (data.reviews.length === 0) { dom.noReviews.style.display = 'block'; return; } renderReviews(data.reviews); renderReviewPagination(data.pagination); } catch (err) { dom.reviewLoading.style.display = 'none'; dom.noReviews.style.display = 'block'; dom.noReviews.textContent = '리뷰를 불러오지 못했습니다.'; } } // ============================================================ // Render Reviews // ============================================================ function renderReviews(reviews) { var fragment = document.createDocumentFragment(); reviews.forEach(function (review) { var card = document.createElement('div'); card.className = 'review-card' + (review.is_blinded ? ' review-card--blinded' : ''); var starsHTML = ''; if (review.rating !== null) { starsHTML = '
' + renderStars(review.rating, 'card') + '
'; } var photoHTML = ''; if (review.photo_url) { photoHTML = '리뷰 사진'; } var actionsHTML = ''; if (window.YS.isLoggedIn && !review.is_blinded) { var btns = ''; if (window.YS.userId === review.user_id) { btns += ''; } else { btns += ''; } actionsHTML = '
' + btns + '
'; } var levelBadge = ''; if (review.level_label && review.level_class) { levelBadge = '' + escapeHTML(review.level_label) + ' '; } card.innerHTML = '
' + '
' + '' + levelBadge + escapeHTML(review.nickname || '탈퇴한 회원') + '' + '' + escapeHTML(review.created_at) + '' + '
' + starsHTML + '
' + '
' + escapeHTML(review.content) + '
' + photoHTML + actionsHTML; fragment.appendChild(card); }); dom.reviewList.appendChild(fragment); } // ============================================================ // Render Review Pagination // ============================================================ function renderReviewPagination(pagination) { var page = pagination.page; var totalPages = pagination.total_pages; if (totalPages <= 1) return; var container = dom.reviewPagination; container.innerHTML = ''; // Previous var prevBtn = createPageBtn('\u2039 이전', 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(createPageBtn('1', 1)); if (startPage > 2) { var dots = document.createElement('span'); dots.className = 'pagination__btn'; dots.textContent = '...'; dots.style.pointerEvents = 'none'; container.appendChild(dots); } } for (var i = startPage; i <= endPage; i++) { var btn = createPageBtn(String(i), i); if (i === page) btn.classList.add('pagination__btn--active'); container.appendChild(btn); } if (endPage < totalPages) { if (endPage < totalPages - 1) { var dots2 = document.createElement('span'); dots2.className = 'pagination__btn'; dots2.textContent = '...'; dots2.style.pointerEvents = 'none'; container.appendChild(dots2); } container.appendChild(createPageBtn(String(totalPages), totalPages)); } // Next var nextBtn = createPageBtn('다음 \u203A', page + 1, page >= totalPages); container.appendChild(nextBtn); } function createPageBtn(label, page, disabled) { var btn = document.createElement('button'); btn.className = 'pagination__btn'; btn.innerHTML = label; btn.disabled = !!disabled; if (!disabled) { btn.addEventListener('click', function () { state.reviewPage = page; loadReviews(); // Scroll to reviews section var section = document.querySelector('.sd__reviews'); if (section) { section.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); } return btn; } // ============================================================ // Heart Toggle // ============================================================ async function toggleHeart() { if (!window.YS.isLoggedIn) { showToast(window.YS.isOwner ? '일반 회원으로 로그인 후 이용해 주세요.' : '로그인 후 이용해 주세요.'); return; } try { var resp = await fetchJSON('/api/heart_toggle.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ store_id: state.storeId }), }); state.isHearted = resp.data.is_hearted; state.heartCount = resp.data.heart_count; // Update button var heartBtn = document.getElementById('heartBtn'); if (heartBtn) { if (state.isHearted) { heartBtn.classList.add('active'); heartBtn.querySelector('.sd__action-icon').innerHTML = '♥'; document.getElementById('heartBtnText').textContent = '즐겨찾기 해제'; } else { heartBtn.classList.remove('active'); heartBtn.querySelector('.sd__action-icon').innerHTML = '♡'; document.getElementById('heartBtnText').textContent = '즐겨찾기'; } document.getElementById('heartBtnCount').textContent = '(' + formatCount(state.heartCount) + ')'; } showToast(state.isHearted ? '즐겨찾기에 추가했습니다.' : '즐겨찾기를 해제했습니다.'); } catch (err) { showToast('처리 중 오류가 발생했습니다.'); } } // ============================================================ // Star Rating Selector // ============================================================ function setupStarSelector() { if (!dom.starSelector) return; var stars = dom.starSelector.querySelectorAll('.review-form__star'); stars.forEach(function (star) { star.addEventListener('click', function () { var rating = parseInt(this.dataset.rating, 10); state.selectedRating = rating; dom.ratingValue.value = rating; updateStarDisplay(rating); }); star.addEventListener('mouseenter', function () { var rating = parseInt(this.dataset.rating, 10); updateStarDisplay(rating); }); }); dom.starSelector.addEventListener('mouseleave', function () { updateStarDisplay(state.selectedRating); }); } function updateStarDisplay(rating) { var stars = dom.starSelector.querySelectorAll('.review-form__star'); stars.forEach(function (star, idx) { if (idx < rating) { star.classList.add('review-form__star--active'); } else { star.classList.remove('review-form__star--active'); } }); } // ============================================================ // Photo File Name Display // ============================================================ function setupPhotoInput() { if (!dom.reviewPhoto) return; dom.reviewPhoto.addEventListener('change', function () { if (this.files && this.files[0]) { dom.photoFileName.textContent = this.files[0].name; } else { dom.photoFileName.textContent = ''; } }); } // ============================================================ // Review Form Submit // ============================================================ function setupReviewForm() { if (!dom.reviewForm) return; dom.reviewForm.addEventListener('submit', async function (e) { e.preventDefault(); if (state.isSubmitting) return; // Validate var rating = parseInt(dom.ratingValue.value, 10); var content = dom.reviewContent.value.trim(); if (rating < 1 || rating > 5) { showFormError('별점을 선택해주세요.'); return; } if (content.length < 10) { showFormError('리뷰 내용을 10자 이상 입력해주세요.'); return; } state.isSubmitting = true; dom.reviewSubmitBtn.disabled = true; dom.reviewSubmitBtn.textContent = '등록 중...'; hideFormError(); try { var formData = new FormData(dom.reviewForm); var resp = await fetch('/api/reviews/create.php', { method: 'POST', body: formData, }); var data = await resp.json(); if (!data.success) { throw new Error(data.error || '리뷰 등록에 실패했습니다.'); } showToast('리뷰가 등록되었습니다!'); // Reset form dom.reviewForm.reset(); state.selectedRating = 0; updateStarDisplay(0); dom.photoFileName.textContent = ''; // Hide form, show already-reviewed dom.reviewFormWrap.style.display = 'none'; dom.alreadyReviewed.style.display = 'block'; // Reload reviews state.reviewPage = 1; loadReviews(); // Reload store to update avg rating reloadStoreRating(); } catch (err) { showFormError(err.message); } finally { state.isSubmitting = false; dom.reviewSubmitBtn.disabled = false; dom.reviewSubmitBtn.textContent = '리뷰 등록'; } }); } function showFormError(msg) { dom.reviewFormError.textContent = msg; dom.reviewFormError.style.display = 'block'; } function hideFormError() { dom.reviewFormError.style.display = 'none'; } // ============================================================ // Reload Store Rating After Review // ============================================================ async function reloadStoreRating() { try { var resp = await fetchJSON('/api/store_detail.php?id=' + state.storeId); var store = resp.data.store; // Update rating display var avgRating = parseFloat(store.avg_rating) || 0; var reviewCount = parseInt(store.review_count, 10) || 0; dom.storeRating.innerHTML = '
' + renderStars(avgRating, 'header') + '
' + '' + store.avg_rating + '' + '(' + formatCount(reviewCount) + '개 리뷰)'; dom.reviewCountBadge.textContent = reviewCount > 0 ? '(' + formatCount(reviewCount) + ')' : ''; } catch (err) { // Silently fail } } // ============================================================ // Report Modal // ============================================================ function setupReportModal() { // Event delegation for report and delete buttons dom.reviewList.addEventListener('click', function (e) { var reportBtn = e.target.closest('.review-card__report-btn'); if (reportBtn) { var reviewId = parseInt(reportBtn.dataset.reviewId, 10); openReportModal(reviewId); return; } var deleteBtn = e.target.closest('.review-card__delete-btn'); if (deleteBtn) { var delReviewId = parseInt(deleteBtn.dataset.reviewId, 10); handleDeleteReview(delReviewId, deleteBtn); } }); // Close modal if (dom.reportModalClose) { dom.reportModalClose.addEventListener('click', closeReportModal); } if (dom.reportCancelBtn) { dom.reportCancelBtn.addEventListener('click', closeReportModal); } // Close on overlay click if (dom.reportModal) { dom.reportModal.addEventListener('click', function (e) { if (e.target === dom.reportModal) { closeReportModal(); } }); } // Submit report if (dom.reportSubmitBtn) { dom.reportSubmitBtn.addEventListener('click', submitReport); } } function openReportModal(reviewId) { if (!window.YS.isLoggedIn) { showToast(window.YS.isOwner ? '일반 회원으로 로그인 후 이용해 주세요.' : '로그인 후 이용해 주세요.'); return; } state.reportingReviewId = reviewId; // Reset radio var radios = dom.reportModal.querySelectorAll('input[name="report_reason"]'); radios.forEach(function (r) { r.checked = false; }); dom.reportModal.style.display = 'flex'; } function closeReportModal() { dom.reportModal.style.display = 'none'; state.reportingReviewId = null; } async function submitReport() { var reason = ''; var radios = dom.reportModal.querySelectorAll('input[name="report_reason"]'); radios.forEach(function (r) { if (r.checked) reason = r.value; }); if (!reason) { showToast('신고 사유를 선택해주세요.'); return; } try { dom.reportSubmitBtn.disabled = true; dom.reportSubmitBtn.textContent = '처리 중...'; var resp = await fetchJSON('/api/reviews/report.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ review_id: state.reportingReviewId, reason: reason, csrf_token: window.YS.csrfToken, }), }); showToast(resp.data.message); closeReportModal(); } catch (err) { showToast(err.message || '신고 처리 중 오류가 발생했습니다.'); } finally { dom.reportSubmitBtn.disabled = false; dom.reportSubmitBtn.textContent = '신고하기'; } } // ============================================================ // Delete Review // ============================================================ async function handleDeleteReview(reviewId, btn) { if (!confirm('리뷰를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) { return; } try { btn.disabled = true; btn.textContent = '삭제 중...'; await fetchJSON('/api/reviews/delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ review_id: reviewId, csrf_token: window.YS.csrfToken, }), }); showToast('리뷰가 삭제되었습니다.'); // Reload reviews state.reviewPage = 1; dom.reviewList.innerHTML = ''; loadReviews(); refreshStoreStats(); } catch (err) { showToast(err.message || '리뷰 삭제 중 오류가 발생했습니다.'); btn.disabled = false; btn.textContent = '삭제'; } } // ============================================================ // Initialize // ============================================================ // ============================================================ // Tab Switching // ============================================================ function setupTabs() { var tabs = $$('.sd__tab'); tabs.forEach(function (tab) { tab.addEventListener('click', function () { var target = this.dataset.tab; tabs.forEach(function (t) { t.classList.remove('sd__tab--active'); }); this.classList.add('sd__tab--active'); // Toggle panels dom.tabAtt.classList.toggle('sd__tab-panel--active', target === 'att'); dom.tabPromo.classList.toggle('sd__tab-panel--active', target === 'promo'); }); }); } function init() { setupTabs(); setupStarSelector(); setupPhotoInput(); setupReviewForm(); setupReportModal(); loadStoreDetail(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();