필터 로직 개선
최종 스크립트 코드 정리
This commit is contained in:
2026-02-22 13:08:50 +09:00
parent 769e0f4b96
commit c3e207e94f
8 changed files with 1013 additions and 1029 deletions

View File

@@ -1,40 +1,42 @@
/** 상품 그리드·페이지네이션 렌더링 */
/**
* productList.js
* 상품 그리드/테이블 렌더링, 지연 로딩 및 페이지네이션 제어
*/
import { state, saveSelection } from './state.js';
import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js';
import { updateSummary } from './main.js';
// --- 터치 및 드래그 관련 전역 변수 및 핸들러 ---
// ==========================================================================
// 1. 터치 및 호버 인터랙션 (Interaction)
// ==========================================================================
window.isDragging = false;
let touchStartX = 0;
let touchStartY = 0;
// 터치 기기 여부 확인
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
/** 터치 시작: 드래그 여부 판단 초기화 */
window.handleTouchStart = function (e) {
window.isDragging = false;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
};
/** 터치 이동: 일정 거리 이상 움직이면 클릭이 아닌 드래그로 간주 */
window.handleTouchMove = function (e) {
const touchX = e.touches[0].clientX;
const touchY = e.touches[0].clientY;
// 10px 이상 움직이면 드래그(스크롤)로 간주
if (Math.abs(touchX - touchStartX) > 10 || Math.abs(touchY - touchStartY) > 10) {
window.isDragging = true;
}
};
window.handleTouchEnd = function (e) {
// 필요한 경우 추가 로직 작성 가능 (현재는 isDragging 상태 유지만으로 충분)
};
window.handleTouchEnd = function (e) {};
// 썸네일 호버 핸들러 (PC에서만 동작하도록 수정)
/** 썸네일 호버: 마우스 위치에 따른 이미지 교체 및 인디케이터 업데이트 */
window.handleThumbnailHover = function (e, id) {
if (isTouchDevice) return; // 모바일에서는 호버 로직 실행 안 함
if (isTouchDevice) return;
const product = state.visibleProducts.find((p) => p.id === id);
if (!product || !product.images || product.images.length <= 1) return;
@@ -49,290 +51,267 @@ window.handleThumbnailHover = function (e, id) {
if (thumb && product.images[index]) {
thumb.style.backgroundImage = `url("${product.images[index]}")`;
}
if (indicators) {
Array.from(indicators).forEach((dot, i) => {
if (i === index) {
dot.classList.add('bg-white', 'scale-125');
dot.classList.remove('bg-white/40');
} else {
dot.classList.remove('bg-white', 'scale-125');
dot.classList.add('bg-white/40');
}
dot.classList.toggle('bg-white', i === index);
dot.classList.toggle('scale-125', i === index);
dot.classList.toggle('bg-white/40', i !== index);
});
}
};
/** 호버 해제: 첫 번째 이미지로 복구 */
window.handleThumbnailLeave = function (id) {
if (isTouchDevice) return;
const product = state.visibleProducts.find((p) => p.id === id);
const thumb = document.getElementById(`thumb-${id}`);
const indicators = document.querySelector(`#indicator-${id}`)?.children;
if (thumb && product) {
thumb.style.backgroundImage = `url("${product.images[0]}")`;
}
if (thumb && product) thumb.style.backgroundImage = `url("${product.images[0]}")`;
if (indicators) {
Array.from(indicators).forEach((dot, i) => {
if (i === 0) {
dot.classList.add('bg-white', 'scale-125');
dot.classList.remove('bg-white/40');
} else {
dot.classList.remove('bg-white', 'scale-125');
dot.classList.add('bg-white/40');
}
dot.classList.toggle('bg-white', i === 0);
dot.classList.toggle('scale-125', i === 0);
dot.classList.toggle('bg-white/40', i !== 0);
});
}
};
// 1. 체크박스 전역 핸들러 등록
/** 개별 체크박스 토글 */
window.toggleSelectItem = function (id) {
if (state.selectedIds.has(id)) {
state.selectedIds.delete(id);
} else {
state.selectedIds.add(id);
}
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
else state.selectedIds.add(id);
saveSelection();
renderProducts(state.currentPage);
updateSummary();
};
// ==========================================================================
// 2. 메인 렌더링 컨트롤러 (Main Rendering)
// ==========================================================================
/**
* 상태에 따른 상품 목록 렌더링 실행
* @param {number} page - 현재 페이지 번호
*/
export function renderProducts(page = 1) {
const grid = document.getElementById('product-grid');
const tableWrapper = document.getElementById('product-table-wrapper');
const tableBody = document.getElementById('product-table-body');
const summaryBar = document.getElementById('selection-summary');
const paginationContainer = document.getElementById('pagination');
if (!grid || !tableWrapper) return;
// [1] 결과 없음 처리
if (state.visibleProducts.length === 0) {
grid.classList.remove('grid');
grid.classList.add('hidden');
tableWrapper.classList.add('hidden');
const emptyMsg = `
<div class="col-span-full flex flex-col items-center justify-center py-20 w-full text-center">
<svg class="w-20 h-20 text-slate-300 dark:text-slate-700 mb-6" viewBox="0 -960 960 960" fill="currentColor">
<path d="M138.5-138.5Q80-197 80-280t58.5-141.5Q197-480 280-480t141.5 58.5Q480-363 480-280t-58.5 141.5Q363-80 280-80t-141.5-58.5ZM824-120 568-376q-12-13-25.5-26.5T516-428q38-24 61-64t23-88q0-75-52.5-127.5T420-760q-75 0-127.5 52.5T240-580q0 6 .5 11.5T242-557q-18 2-39.5 8T164-535q-2-11-3-22t-1-23q0-109 75.5-184.5T420-840q109 0 184.5 75.5T680-580q0 43-13.5 81.5T629-428l251 252-56 56Zm-615-61 71-71 70 71 29-28-71-71 71-71-28-28-71 71-71-71-28 28 71 71-71 71 28 28Z"/>
</svg>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
</div>`;
grid.innerHTML = emptyMsg;
grid.classList.remove('hidden');
if (paginationContainer) paginationContainer.innerHTML = '';
renderEmpty(grid, tableWrapper, paginationContainer);
return;
}
if (state.viewMode === 'grid') {
grid.classList.remove('hidden');
grid.classList.add('grid');
tableWrapper.classList.add('hidden');
if (summaryBar) {
summaryBar.classList.remove('flex');
summaryBar.classList.add('hidden');
}
} else {
grid.classList.remove('grid');
grid.classList.add('hidden');
tableWrapper.classList.remove('hidden');
updateSummary();
}
// [2] 페이지 데이터 슬라이싱
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
// [3] 뷰 모드에 따른 렌더링
if (state.viewMode === 'grid') {
grid.innerHTML = '';
pagedProducts.forEach((product) => {
const isSold = STATUS_META[product.status]?.soldOut === true;
const isNonSale = product.status === '미판매';
const conditionKey = product.specs?.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
const conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || '';
grid.insertAdjacentHTML(
'beforeend',
`
<div class="product-card group flex flex-col gap-4 cursor-pointer"
data-id="${product.id}"
onclick="if(!window.isDragging) window.openModal('${product.id}')"
ontouchstart="window.handleTouchStart(event)"
ontouchmove="window.handleTouchMove(event)"
ontouchend="window.handleTouchEnd(event)"
onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmouseleave="window.handleThumbnailLeave('${product.id}')">
<div class="relative w-full aspect-card bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm">
<div id="thumb-${product.id}"
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 transition-all duration-500 ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, transform;">
</div>
<div id="thumb-fade-${product.id}"
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 opacity-0 pointer-events-none transition-all duration-500 ${isSold ? 'grayscale' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, opacity, transform;">
</div>
<div class="absolute top-3 left-3 z-10">
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">${product.status}</span>
</div>
${
!isSold && product.images?.length > 1
? `
<div id="indicator-${product.id}" class="absolute bottom-3 left-1/2 -translate-x-1/2 hidden md:flex gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-10">
${product.images
.map(
(_, i) => `
<div class="w-1.5 h-1.5 rounded-full transition-all duration-300 shadow-sm ${i === 0 ? 'bg-white scale-125' : 'bg-white/40'}"></div>
`,
)
.join('')}
</div>`
: ''
}
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col justify-between items-start sm:items-baseline gap-1">
<h3 class="text-slate-900 dark:text-white text-base font-semibold leading-tight break-keep ${isSold ? 'line-through text-slate-400' : ''}">${product.title}</h3>
<p class="text-base font-bold whitespace-nowrap ${isNonSale ? 'text-slate-400 font-medium text-xs uppercase' : 'text-slate-900 dark:text-white'}">
${isNonSale ? 'Not for Sale' : `${product.currency || '₩'}${product.price.toLocaleString()}`}
</p>
</div>
<div class="flex flex-col">
${conditionDisplay ? `<span class="text-[11px] font-medium text-slate-400 mb-0.5">${conditionDisplay}</span>` : ''}
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1 italic">${product.description}</p>
</div>
</div>
</div>`,
);
});
setupLazyLoading();
renderGridView(grid, tableWrapper, pagedProducts);
} else {
// 테이블 렌더링 로직 (생략 없이 유지)
tableBody.innerHTML = pagedProducts
.map((product) => {
const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false;
const conditionKey = product.specs.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
let conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || '상세 설명 참고 ';
let conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}"
onclick="if(event.target.type !== 'checkbox') {
${isSold ? "alert('판매 완료된 상품입니다.');" : `window.openModal('${product.id}')`}
}">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox" class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
${state.selectedIds.has(product.id) ? 'checked' : ''} ${isSelectable ? '' : 'disabled'}
onchange="window.toggleSelectItem('${product.id}')">
</td>
<td class="py-4 px-4 font-semibold ${isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white'}">${product.title}</td>
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td>
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white">₩${product.price.toLocaleString()}</td>
<td class="hidden lg:table-cell py-4 px-4 text-center">
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
</td>
</tr>`;
})
.join('');
renderTableView(grid, tableWrapper, pagedProducts);
}
const selectAllCheck = document.getElementById('select-all-current');
if (selectAllCheck) {
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const currentSelectableItems = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE).filter((p) => STATUS_META[p.status]?.selectable !== false);
selectAllCheck.checked = currentSelectableItems.length > 0 && currentSelectableItems.every((p) => state.selectedIds.has(p.id));
}
if (typeof renderPagination === 'function') renderPagination();
updateSelectAllCheckbox(page);
renderPagination();
}
/** 검색 결과가 없을 때의 UI */
function renderEmpty(grid, tableWrapper, paginationContainer) {
grid.classList.remove('grid');
grid.classList.add('hidden');
tableWrapper.classList.add('hidden');
const emptyMsg = `
<div class="col-span-full flex flex-col items-center justify-center py-20 w-full text-center">
<svg class="w-20 h-20 text-slate-300 dark:text-slate-700 mb-6" viewBox="0 -960 960 960" fill="currentColor">
<path d="M138.5-138.5Q80-197 80-280t58.5-141.5Q197-480 280-480t141.5 58.5Q480-363 480-280t-58.5 141.5Q363-80 280-80t-141.5-58.5ZM824-120 568-376q-12-13-25.5-26.5T516-428q38-24 61-64t23-88q0-75-52.5-127.5T420-760q-75 0-127.5 52.5T240-580q0 6 .5 11.5T242-557q-18 2-39.5 8T164-535q-2-11-3-22t-1-23q0-109 75.5-184.5T420-840q109 0 184.5 75.5T680-580q0 43-13.5 81.5T629-428l251 252-56 56Zm-615-61 71-71 70 71 29-28-71-71 71-71-28-28-71 71-71-71-28 28 71 71-71 71 28 28Z"/>
</svg>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
</div>`;
grid.innerHTML = emptyMsg;
grid.classList.remove('hidden');
if (paginationContainer) paginationContainer.innerHTML = '';
}
// ==========================================================================
// 3. 그리드 & 테이블 뷰 상세 (View Details)
// ==========================================================================
/** 그리드 뷰 렌더링 */
function renderGridView(grid, tableWrapper, products) {
grid.classList.replace('hidden', 'grid');
tableWrapper.classList.add('hidden');
const summaryBar = document.getElementById('selection-summary');
if (summaryBar) summaryBar.classList.add('hidden');
grid.innerHTML = products.map(product => createProductCardHTML(product)).join('');
setupLazyLoading();
}
/** 그리드 개별 카드 HTML */
function createProductCardHTML(product) {
const isSold = STATUS_META[product.status]?.soldOut === true;
const isNonSale = product.status === '미판매';
const conditionConfig = PRODUCT_CONDITIONS[product.specs?.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : product.specs?.condition || '';
return `
<div class="product-card group flex flex-col gap-4 cursor-pointer"
data-id="${product.id}"
onclick="if(!window.isDragging) window.openModal('${product.id}')"
ontouchstart="window.handleTouchStart(event)"
ontouchmove="window.handleTouchMove(event)"
ontouchend="window.handleTouchEnd(event)"
onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmouseleave="window.handleThumbnailLeave('${product.id}')">
<div class="relative w-full aspect-card bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm">
<div id="thumb-${product.id}"
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 transition-all duration-500 ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, transform;">
</div>
<div class="absolute top-3 left-3 z-10">
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">${product.status}</span>
</div>
${!isSold && product.images?.length > 1 ? `
<div id="indicator-${product.id}" class="absolute bottom-3 left-1/2 -translate-x-1/2 hidden md:flex gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-10">
${product.images.map((_, i) => `<div class="w-1.5 h-1.5 rounded-full transition-all duration-300 shadow-sm ${i === 0 ? 'bg-white scale-125' : 'bg-white/40'}"></div>`).join('')}
</div>` : ''}
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col justify-between items-start sm:items-baseline gap-1">
<h3 class="text-slate-900 dark:text-white text-base font-semibold leading-tight break-keep ${isSold ? 'line-through text-slate-400' : ''}">${product.title}</h3>
<p class="text-base font-bold whitespace-nowrap ${isNonSale ? 'text-slate-400 font-medium text-xs uppercase' : 'text-slate-900 dark:text-white'}">
${isNonSale ? 'Not for Sale' : `${product.currency || '₩'}${product.price.toLocaleString()}`}
</p>
</div>
<div class="flex flex-col">
${conditionDisplay ? `<span class="text-[11px] font-medium text-slate-400 mb-0.5">${conditionDisplay}</span>` : ''}
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1 italic">${product.description}</p>
</div>
</div>
</div>`;
}
/** 테이블 뷰 렌더링 */
function renderTableView(grid, tableWrapper, products) {
grid.classList.add('hidden');
tableWrapper.classList.remove('hidden');
const tableBody = document.getElementById('product-table-body');
tableBody.innerHTML = products.map(product => createTableRowHTML(product)).join('');
updateSummary();
}
/** 테이블 개별 행 HTML */
function createTableRowHTML(product) {
const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false;
const conditionConfig = PRODUCT_CONDITIONS[product.specs.condition];
const conditionDisplay = conditionConfig ? conditionConfig.label : '상세 설명 참고 ';
const conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}"
onclick="if(event.target.type !== 'checkbox') { ${isSold ? "alert('판매 완료된 상품입니다.');" : `window.openModal('${product.id}')`} }">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox" class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
${state.selectedIds.has(product.id) ? 'checked' : ''} ${isSelectable ? '' : 'disabled'}
onchange="window.toggleSelectItem('${product.id}')">
</td>
<td class="py-4 px-4 font-semibold ${isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white'}">${product.title}</td>
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td>
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white">₩${product.price.toLocaleString()}</td>
<td class="hidden lg:table-cell py-4 px-4 text-center">
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
</td>
</tr>`;
}
// ==========================================================================
// 4. 유틸리티 및 페이지네이션 (Utils)
// ==========================================================================
/** 전체 선택 체크박스 상태 동기화 */
function updateSelectAllCheckbox(page) {
const selectAllCheck = document.getElementById('select-all-current');
if (!selectAllCheck) return;
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const currentSelectableItems = state.visibleProducts
.slice(startIndex, startIndex + ITEMS_PER_PAGE)
.filter((p) => STATUS_META[p.status]?.selectable !== false);
selectAllCheck.checked = currentSelectableItems.length > 0 &&
currentSelectableItems.every((p) => state.selectedIds.has(p.id));
}
/** Intersection Observer를 이용한 썸네일 지연 로딩 */
function setupLazyLoading() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const card = entry.target;
const productId = card.getAttribute('data-id');
// state.js 등에서 가져온 데이터 활용
const product = state.visibleProducts.find((p) => p.id === productId);
const thumb = document.getElementById(`thumb-${productId}`);
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const card = entry.target;
const productId = card.getAttribute('data-id');
const product = state.visibleProducts.find((p) => p.id === productId);
const thumb = document.getElementById(`thumb-${productId}`);
if (product && thumb) {
// 1. 첫 번째 이미지 로드
thumb.style.backgroundImage = `url("${product.images[0]}")`;
// 2. 나머지 이미지 프리로드 (반짝임 방지)
if (!STATUS_META[product.status]?.soldOut && product.images.length > 1) {
product.images.slice(1).forEach((url) => {
const img = new Image();
img.src = url;
});
}
if (product && thumb) {
thumb.style.backgroundImage = `url("${product.images[0]}")`;
// 마우스 호버를 대비해 나머지 이미지 미리 로드
if (!STATUS_META[product.status]?.soldOut && product.images.length > 1) {
product.images.slice(1).forEach(url => { const img = new Image(); img.src = url; });
}
observer.unobserve(card);
}
});
},
{ threshold: 0.1 },
);
observer.unobserve(card);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.product-card').forEach((card) => observer.observe(card));
}
/** 페이지네이션 버튼 렌더링 */
export function renderPagination() {
const container = document.getElementById('pagination');
if (!container) return;
const totalPages = Math.ceil(state.visibleProducts.length / ITEMS_PER_PAGE);
const { currentPage } = state;
let html = `<button onclick="changePage(${currentPage - 1})" class="size-10 flex items-center justify-center ${currentPage === 1 ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M15 18l-6-6 6-6" />
</svg>
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5"><path d="M15 18l-6-6 6-6" /></svg>
</button>`;
for (let i = 1; i <= totalPages; i++) {
html += `<button onclick="changePage(${i})" class="size-10 font-bold rounded-lg ${i === currentPage ? 'bg-primary text-white' : 'text-slate-500'}">${i}</button>`;
}
html += `<button onclick="changePage(${currentPage + 1})" class="size-10 flex items-center justify-center ${currentPage === totalPages ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M9 18l6-6-6-6" />
</svg>
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5"><path d="M9 18l6-6-6-6" /></svg>
</button>`;
container.innerHTML = html;
}
/** 페이지 변경 처리 */
export function changePage(page) {
state.currentPage = page;
renderProducts(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/** 모든 필터를 초기 상태로 되돌리는 함수 */
function resetAllFilters() {
state.searchKeyword = '';
const searchInput = document.getElementById('search-input');
if (searchInput) searchInput.value = '';
state.activeCategories.clear();
state.activeCategories.add('All');
// 상태 필터 초기화 (config에서 defaultActive인 것만)
import('./config.js').then(({ STATUS_FILTERS }) => {
state.activeStatuses.clear();
STATUS_FILTERS.filter((f) => f.defaultActive).forEach((f) => state.activeStatuses.add(f.key));
// UI 전체 갱신
applyFilters();
renderStatusChips();
renderCategoryChips(productsData);
});
}
}