// scripts/app.js 상단 import products from '../data/index.js'; // 이제 products는 모든 카테고리가 합쳐지고 날짜순으로 정렬된 상태입니다. console.log('Total products loaded:', products.length); const ITEMS_PER_PAGE = 20; let currentPage = 1; let activeCategories = new Set(['All']); let visibleProducts = products; let searchKeyword = ''; const VISIBILITY_CONFIG = { showUnlisted: false, // 🔥 미판매 노출 여부 showSold: true, // 🔥 판매완료 노출 여부 }; const STATUS_META = { 미판매: { selectable: false, defaultVisible: false, soldOut: false, }, 판매예정: { selectable: true, defaultVisible: true, soldOut: false, }, 판매중: { selectable: true, defaultVisible: true, soldOut: false, }, 판매완료: { selectable: true, defaultVisible: false, soldOut: true, }, }; const STATUS_FILTERS = [ { key: '판매중', label: '판매중', defaultActive: true, visible: true, }, { key: '판매예정', label: '판매 예정', defaultActive: true, visible: true, }, { key: '미판매', label: '미판매', defaultActive: false, visible: VISIBILITY_CONFIG.showUnlisted, }, { key: '판매완료', label: '판매완료', defaultActive: false, visible: VISIBILITY_CONFIG.showSold, }, ]; const STATUS_ORDER = { 판매중: 0, 판매예정: 1, 미판매: 2, 판매완료: 3, // 🔥 항상 맨 뒤 }; const STATUS_COLOR = { 판매중: 'bg-primary/10 text-primary border-primary/30', 판매예정: 'bg-amber-400/10 text-amber-600 border-amber-400/30', 판매완료: 'bg-slate-400/10 text-slate-500 border-slate-400/30', 미판매: 'bg-slate-200/10 text-slate-400 border-slate-300/30', }; let activeStatuses = new Set( Object.entries(STATUS_META) .filter(([_, meta]) => meta.defaultVisible) .map(([status]) => status), ); // openModal 함수 내부에 추가하거나 전역으로 설정 const copyBtn = document.getElementById('copy-link-btn'); const copyBtnText = document.getElementById('copy-btn-text'); copyBtn.onclick = () => { // 현재 도메인 + 제품 ID 쿼리 조합 const shareUrl = `${window.location.origin}${window.location.pathname}?id=${product.id}`; navigator.clipboard.writeText(shareUrl).then(() => { copyBtnText.textContent = '링크가 복사되었습니다!'; copyBtn.classList.replace('bg-slate-900', 'bg-green-600'); setTimeout(() => { copyBtnText.textContent = '상품 링크 복사하기'; copyBtn.classList.replace('bg-green-600', 'bg-slate-900'); }, 2000); }); }; function getStatusChipClass(status, isActive) { const base = STATUS_COLOR[status] ?? ''; if (isActive) { return ` ${base} opacity-100 shadow-sm `; } // 🔥 비활성 return ` bg-slate-50 text-slate-400 border-slate-200 opacity-30 grayscale hover:opacity-50 `; } function renderStatusChips() { const container = document.getElementById('status-chips'); if (!container) return; container.innerHTML = ''; STATUS_FILTERS.filter((f) => f.visible).forEach(({ key, label }) => { const isActive = activeStatuses.has(key); const chip = document.createElement('button'); chip.className = ` status-chip px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 border ${getStatusChipClass(key, isActive)} `; chip.textContent = label; chip.onclick = () => toggleStatusFilter(key); container.appendChild(chip); }); } function toggleStatusFilter(status) { if (activeStatuses.has(status)) { activeStatuses.delete(status); } else { activeStatuses.add(status); } // 최소 1개는 유지 if (activeStatuses.size === 0) { STATUS_FILTERS.filter((f) => f.defaultActive).forEach((f) => activeStatuses.add(f.key)); } applyFilters(); renderStatusChips(); } const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', (e) => { searchKeyword = e.target.value.trim().toLowerCase(); applyFilters(); }); } function applyFilters() { currentPage = 1; visibleProducts = products .filter((product) => { // 🔒 미판매 강제 차단 if (product.status === '미판매' && !VISIBILITY_CONFIG.showUnlisted) { return false; } // 🔒 판매완료 기본 숨김 if (product.status === '판매완료' && !VISIBILITY_CONFIG.showSold) { return false; } const statusMatch = activeStatuses.has(product.status); const categoryMatch = activeCategories.has('All') || activeCategories.has(product.category); const searchMatch = searchKeyword === '' || product.title.toLowerCase().includes(searchKeyword); return statusMatch && categoryMatch && searchMatch; }) // 🔥 여기서 정렬 .sort((a, b) => { const aOrder = STATUS_ORDER[a.status] ?? 999; const bOrder = STATUS_ORDER[b.status] ?? 999; return aOrder - bOrder; }); renderProducts(currentPage); } /** * 1. 상품 목록 렌더링 */ export function renderProducts(page) { const grid = document.getElementById('product-grid'); if (!grid) return; grid.innerHTML = ''; const startIndex = (page - 1) * ITEMS_PER_PAGE; const pagedProducts = visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE); pagedProducts.forEach((product) => { const isSold = STATUS_META[product.status]?.soldOut === true; const cardHtml = `
${product.status}

${product.title}

${product.currency}${product.price.toLocaleString()}

${product.description}

`; grid.insertAdjacentHTML('beforeend', cardHtml); }); renderPagination(); } /** * 2. 모달 열기 및 데이터 채우기 */ window.openModal = (id) => { const product = products.find((p) => p.id === id); if (!product) return; const modal = document.getElementById('product-modal'); const images = product.images; // --- 1. 이미지 및 UI 초기화 로직 --- const loopImages = [images[images.length - 1], ...images, images[0]]; const mainImagesHtml = loopImages .map( (img) => `
`, ) .join(''); const thumbnailsHtml = product.images .map( (img, idx) => ` `, ) .join(''); const dotsHtml = product.images .map( (_, idx) => ` `, ) .join(''); // --- 2. 데이터 주입 --- document.getElementById('modal-main-carousel').innerHTML = mainImagesHtml; document.getElementById('modal-thumbnails').innerHTML = thumbnailsHtml; document.getElementById('modal-dots').innerHTML = dotsHtml; document.getElementById('modal-title').textContent = product.title; document.getElementById('modal-price').textContent = `${product.currency}${product.price.toLocaleString()}`; // 카테고리 및 상태 const modalCategory = document.getElementById('modal-category'); if (modalCategory) modalCategory.textContent = product.category; const modalStatus = document.getElementById('modal-status'); if (modalStatus) { modalStatus.textContent = product.status; const statusStyles = { 판매중: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', 판매예정: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', 판매완료: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', 미판매: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400', }; modalStatus.className = 'px-2.5 py-1 rounded-lg text-xs font-bold uppercase tracking-wider ' + (statusStyles[product.status] || statusStyles['미판매']); } // 커스텀 태그 const customTagElement = document.getElementById('modal-custom-tag'); if (product.customTag?.trim()) { customTagElement.textContent = product.customTag; customTagElement.classList.remove('hidden'); } else { customTagElement.classList.add('hidden'); } // 상세 설명 const modalDesc = document.getElementById('modal-desc'); if (modalDesc) { modalDesc.innerHTML = Array.isArray(product.fullDescription) ? product.fullDescription.join('
') : product.fullDescription || ''; } // --- 3. [핵심] 링크 복사 버튼 이벤트 바인딩 --- const copyBtn = document.getElementById('copy-link-btn'); const copyBtnText = document.getElementById('copy-btn-text'); if (copyBtn) { copyBtn.onclick = () => { const shareUrl = `${window.location.origin}${window.location.pathname}?id=${product.id}`; navigator.clipboard.writeText(shareUrl).then(() => { if (copyBtnText) copyBtnText.textContent = '링크가 복사되었습니다!'; copyBtn.classList.add('!bg-green-600'); setTimeout(() => { if (copyBtnText) copyBtnText.textContent = '상품 링크 복사하기'; copyBtn.classList.remove('!bg-green-600'); }, 2000); }); }; } // --- 4. 모달 활성화 및 초기 위치 설정 --- modal.classList.remove('hidden'); document.body.style.overflow = 'hidden'; const container = document.getElementById('modal-main-carousel-container'); container.style.scrollBehavior = 'auto'; container.scrollLeft = container.clientWidth; // 캐러셀 초기화 initBetterCarousel(container, images.length); }; /** * 모달 닫기 (URL 정리 기능 포함) */ window.closeModal = () => { document.getElementById('product-modal').classList.add('hidden'); document.body.style.overflow = 'auto'; const cleanUrl = window.location.origin + window.location.pathname; window.history.replaceState(null, '', cleanUrl); }; function initBetterCarousel(container, originalLength) { let isDragging = false; let startX = 0; let startScroll = 0; let startTime = 0; const width = () => container.clientWidth; container.addEventListener('mousedown', start); container.addEventListener('touchstart', start, { passive: true }); function start(e) { isDragging = true; startX = e.touches ? e.touches[0].pageX : e.pageX; startScroll = container.scrollLeft; startTime = Date.now(); } container.addEventListener('mousemove', move); container.addEventListener('touchmove', move, { passive: false }); function move(e) { if (!isDragging) return; const x = e.touches ? e.touches[0].pageX : e.pageX; container.scrollLeft = startScroll - (x - startX); } container.addEventListener('mouseup', end); container.addEventListener('mouseleave', end); container.addEventListener('touchend', end); function end(e) { if (!isDragging) return; isDragging = false; const delta = container.scrollLeft - startScroll; const elapsed = Date.now() - startTime; const direction = Math.abs(delta) > width() * 0.1 || elapsed < 200 ? (delta > 0 ? 1 : -1) : 0; let index = Math.round(startScroll / width()) + direction; container.style.scrollBehavior = 'smooth'; container.scrollTo({ left: index * width() }); // 무한 루프 보정 setTimeout(() => { container.style.scrollBehavior = 'auto'; if (index === 0) { container.scrollLeft = width() * originalLength; } if (index === originalLength + 1) { container.scrollLeft = width(); } syncModalUI(originalLength); }, 300); } } /** * 이미지 슬라이드와 UI(Dots, Thumbs) 동기화 */ function syncModalUI(originalLength) { const container = document.getElementById('modal-main-carousel-container'); const index = getRealIndex(container, originalLength); document.querySelectorAll('.modal-thumb-item').forEach((t, i) => { t.classList.toggle('border-primary', i === index); t.classList.toggle('opacity-100', i === index); t.classList.toggle('opacity-70', i !== index); }); document.querySelectorAll('.modal-dot-item').forEach((d, i) => { d.classList.toggle('bg-primary', i === index); d.classList.toggle('w-4', i === index); d.classList.toggle('bg-gray-300', i !== index); d.classList.toggle('w-2', i !== index); }); ensureThumbnailVisible(index); } /** * 3. 모달 내 이미지 스크롤 및 UI 동기화 */ window.scrollToImage = (index) => { const container = document.getElementById('modal-main-carousel-container'); if (!container) return; container.scrollTo({ left: container.clientWidth * (index + 1), // 🔥 중요 behavior: 'smooth', }); }; /** * 5. 기타 (페이지네이션, 모달 닫기) */ function renderPagination() { const container = document.getElementById('pagination'); if (!container) return; const totalPages = Math.ceil(visibleProducts.length / ITEMS_PER_PAGE); let html = ``; for (let i = 1; i <= totalPages; i++) { html += ``; } html += ``; container.innerHTML = html; } window.changePage = (page) => { currentPage = page; renderProducts(currentPage); window.scrollTo({ top: 0, behavior: 'smooth' }); }; // 초기 실행 document.addEventListener('DOMContentLoaded', () => renderProducts(currentPage)); document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') return; const modal = document.getElementById('product-modal'); if (!modal || modal.classList.contains('hidden')) return; closeModal(); }); const thumbnailContainer = document.getElementById('modal-thumbnails'); function ensureThumbnailVisible(index) { const container = document.getElementById('modal-thumbnails'); if (!container) return; const thumbs = container.querySelectorAll('.modal-thumb-item'); const active = thumbs[index]; if (!active) return; const cRect = container.getBoundingClientRect(); const tRect = active.getBoundingClientRect(); const isVisible = tRect.top >= cRect.top && tRect.bottom <= cRect.bottom; if (!isVisible) { active.scrollIntoView({ behavior: 'smooth', block: 'center', }); } } function getRealIndex(container, originalLength) { let rawIndex = Math.round(container.scrollLeft / container.clientWidth); let index = rawIndex - 1; // 클론 보정 if (index < 0) index = originalLength - 1; if (index >= originalLength) index = 0; return index; } // 카테고리 필터 function getCategories(products) { return ['All', ...new Set(products.map((p) => p.category))]; } function renderCategoryChips(products) { const container = document.getElementById('filter-chips'); if (!container) return; const categories = ['All', ...new Set(products.map((p) => p.category))]; container.innerHTML = ''; categories.forEach((cat) => { const isActive = activeCategories.has(cat); const chip = document.createElement('button'); chip.className = ` filter-chip px-4 py-2 rounded-full text-sm font-medium transition border ${isActive ? 'bg-primary text-white border-primary' : 'bg-slate-50 text-slate-600 border-slate-200'} `; chip.textContent = cat; chip.dataset.category = cat; chip.onclick = () => { toggleCategory(cat); }; container.appendChild(chip); }); } function toggleCategory(category) { if (category === 'All') { activeCategories.clear(); activeCategories.add('All'); } else { activeCategories.delete('All'); activeCategories.has(category) ? activeCategories.delete(category) : activeCategories.add(category); if (activeCategories.size === 0) { activeCategories.add('All'); } } renderCategoryChips(products); applyFilters(); } function bindCategoryFilter(products) { const chips = document.querySelectorAll('.filter-chip'); chips.forEach((chip) => { chip.addEventListener('click', () => { const category = chip.dataset.category; if (category === 'All') { activeCategories.clear(); activeCategories.add('All'); } else { activeCategories.delete('All'); if (activeCategories.has(category)) { activeCategories.delete(category); } else { activeCategories.add(category); } // 아무 것도 없으면 All로 복귀 if (activeCategories.size === 0) { activeCategories.add('All'); } } applyFilters(); }); }); } // 카테고리 필터 renderCategoryChips(products); bindCategoryFilter(products); // updateChipUI(); // 상태 필터 (정책 기반) renderStatusChips(); // 🔥 최초 필터 적용 (이게 첫 렌더) applyFilters(); // 초기 실행 시 호출 document.addEventListener('DOMContentLoaded', () => { renderProducts(currentPage); checkUrlAndOpenModal(); }); function checkUrlAndOpenModal() { const params = new URLSearchParams(window.location.search); const productId = params.get('id'); // URL에서 가져온 ID (문자열) if (productId) { // 데이터의 id와 URL의 id를 모두 문자열로 변환하여 비교 const product = products.find((p) => String(p.id) === productId); if (product) { // DOM 렌더링 시간을 고려해 약간의 지연 후 모달 오픈 setTimeout(() => openModal(product.id), 100); } } } // [최종 ID 생성기] 매번 완전히 새로운 8자리 난수 출력 const newId = Math.random().toString(36).substring(2, 10); console.log(`%c[NEW ID for data.js]: ${newId}`, 'color: #137fec; font-weight: bold; border: 1px solid #137fec; padding: 2px 5px; border-radius: 4px;');