- 카드 전환 방식 변경

- 필터 조건 선택 가능하게 변경 등
This commit is contained in:
2026-02-12 01:35:44 +09:00
parent d9dea9581c
commit a7817d2113
17 changed files with 440 additions and 225 deletions

View File

@@ -67,6 +67,7 @@ export function renderProducts(page = 1) {
if (state.viewMode === 'grid') {
grid.innerHTML = '';
pagedProducts.forEach((product) => {
// 1. 상태 판별
const isSold = STATUS_META[product.status]?.soldOut === true;
const isNonSale = product.status === '미판매'; // 상태값이 '미판매'일 때
@@ -76,82 +77,69 @@ export function renderProducts(page = 1) {
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="window.openModal('${product.id}')"
${!isSold ? `onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmouseleave="window.handleThumbnailLeave('${product.id}')"` : ''}>
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}')"
${!isSold ? `
onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmouseleave="window.handleThumbnailLeave('${product.id}')"
ontouchstart="window.handleTouchStart(event)"
ontouchmove="window.handleTouchMove(event, '${product.id}')"
ontouchend="window.handleTouchEnd(event, '${product.id}')"
` : `
ontouchend="window.handleTouchEnd(event, '${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 transform ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105 transition-transform duration-500'}"
style="background-image: none; will-change: background-image;">
</div>
<div class="absolute top-3 left-3">
<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 flex gap-1.5 opacity-0 group-hover:opacity-100 transition-all duration-300 pointer-events-none">
${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 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 transform transition-transform duration-500 ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image;">
</div>
<div class="flex flex-col gap-1.5">
<div class="flex flex-col sm:flex-row 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.price.toLocaleString()}`}
</p>
</div>
<div id="thumb-fade-${product.id}"
class="w-full h-full bg-center bg-no-repeat bg-cover absolute inset-0 opacity-0 transition-opacity duration-300 pointer-events-none transform transition-transform duration-500 ${isSold ? 'grayscale' : 'group-hover:scale-105'}"
style="background-image: none; will-change: background-image, opacity;">
</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 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 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>
`);
});
// 지연 로딩(Lazy Loading) 관찰자 설정
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;
});
}
}
observer.unobserve(card);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.product-card').forEach(card => observer.observe(card));
setupLazyLoading();
} else {
// 테이블 렌더링
@@ -204,6 +192,36 @@ export function renderProducts(page = 1) {
}
}
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}`);
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;
});
}
}
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;