[260211]
- 썸네일 슬라이드 기능 추가 - 미판매 상품 가격 표시 안함 추후 해야할일: 이미지 webp로 변환
This commit is contained in:
@@ -11,7 +11,7 @@ export const STATUS_META = {
|
||||
미판매: {
|
||||
selectable: false, // 체크박스 선택 불가
|
||||
isDefaultActive: false, // 초기 로드 시 미체크 상태
|
||||
isSystemVisible: false, // 아예 리스트/필터에서 제외 (완전 숨김)
|
||||
isSystemVisible: true, // 아예 리스트/필터에서 제외 (완전 숨김)
|
||||
soldOut: false,
|
||||
},
|
||||
판매예정: {
|
||||
@@ -30,7 +30,7 @@ export const STATUS_META = {
|
||||
selectable: false,
|
||||
isDefaultActive: false, // 초기에는 안 보이지만, 사용자가 필터 클릭하면 보임
|
||||
isSystemVisible: true,
|
||||
soldOut: true, // 이미지에 SOLD OUT 표시
|
||||
soldOut: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -256,3 +256,59 @@ window.exportToExcel = () => {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
let currentHoverIndex = -1;
|
||||
|
||||
window.handleThumbnailHover = (event, productId) => {
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
if (!product || product.images.length <= 1) return;
|
||||
|
||||
const container = event.currentTarget;
|
||||
const thumbImg = document.getElementById(`thumb-${productId}`);
|
||||
const indicator = document.getElementById(`indicator-${productId}`);
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const index = Math.min(Math.floor((x / rect.width) * product.images.length), product.images.length - 1);
|
||||
|
||||
// [핵심] 인덱스가 실제로 바뀔 때만 딱 한 번 실행
|
||||
if (currentHoverIndex !== index) {
|
||||
currentHoverIndex = index;
|
||||
const targetImgUrl = product.images[index];
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// 이미 프리로드된 상태라면 즉시 교체됨
|
||||
thumbImg.style.backgroundImage = `url("${targetImgUrl}")`;
|
||||
|
||||
if (indicator) {
|
||||
Array.from(indicator.children).forEach((dot, i) => {
|
||||
if (i === index) {
|
||||
dot.style.backgroundColor = 'white';
|
||||
dot.style.transform = 'scale(1.2)';
|
||||
dot.style.opacity = '1';
|
||||
} else {
|
||||
dot.style.backgroundColor = 'rgba(255, 255, 255, 0.4)';
|
||||
dot.style.transform = 'scale(1)';
|
||||
dot.style.opacity = '0.7';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.handleThumbnailLeave = (productId) => {
|
||||
currentHoverIndex = -1; // 인덱스 초기화
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
if (!product) return;
|
||||
|
||||
const thumbImg = document.getElementById(`thumb-${productId}`);
|
||||
const indicator = document.getElementById(`indicator-${productId}`);
|
||||
|
||||
thumbImg.style.backgroundImage = `url("${product.images[0]}")`;
|
||||
if (indicator) {
|
||||
Array.from(indicator.children).forEach((dot, i) => {
|
||||
dot.style.backgroundColor = i === 0 ? 'white' : 'rgba(255,255,255,0.3)';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,137 +24,184 @@ export function renderProducts(page = 1) {
|
||||
|
||||
if (!grid || !tableWrapper) return;
|
||||
|
||||
// 1. 결과가 0개인 경우 (그리드/테이블 공통 안내)
|
||||
// 1. 결과가 0개인 경우 안내
|
||||
if (state.visibleProducts.length === 0) {
|
||||
grid.classList.remove('grid', 'hidden');
|
||||
grid.innerHTML = `
|
||||
<div class="flex flex-col items-center justify-center py-20 w-full text-center">
|
||||
grid.classList.remove('grid');
|
||||
grid.classList.add('hidden');
|
||||
tableWrapper.classList.add('hidden');
|
||||
|
||||
// 검색 결과 없음 메시지를 표시할 별도의 컨테이너가 없다면 grid 영역을 빌려 씁니다.
|
||||
const emptyMsg = `
|
||||
<div class="col-span-full flex flex-col items-center justify-center py-20 w-full text-center">
|
||||
<span class="material-symbols-outlined text-6xl text-slate-300 dark:text-slate-700 mb-4">search_off</span>
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
|
||||
</div>
|
||||
`;
|
||||
tableWrapper.classList.add('hidden');
|
||||
grid.innerHTML = emptyMsg;
|
||||
grid.classList.remove('hidden');
|
||||
if (paginationContainer) paginationContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 뷰 모드에 따른 컨테이너 노출 설정
|
||||
// 2. 뷰 모드 설정 및 컨테이너 노출 정리 (hidden/flex/grid 충돌 방지)
|
||||
if (state.viewMode === 'grid') {
|
||||
// 그리드 활성화
|
||||
grid.classList.remove('hidden');
|
||||
grid.classList.add('grid');
|
||||
|
||||
// 테이블 및 요약바 비활성화
|
||||
tableWrapper.classList.add('hidden');
|
||||
if (summaryBar) {
|
||||
summaryBar.classList.remove('flex'); // flex 제거
|
||||
summaryBar.classList.remove('flex');
|
||||
summaryBar.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
// 그리드 비활성화
|
||||
grid.classList.remove('grid');
|
||||
grid.classList.add('hidden');
|
||||
|
||||
// 테이블 활성화
|
||||
tableWrapper.classList.remove('hidden');
|
||||
|
||||
// 요약바 노출 여부는 데이터 상태에 따라 updateSummary에서 결정
|
||||
updateSummary();
|
||||
updateSummary(); // 테이블일 때만 요약바 노출 여부 결정
|
||||
}
|
||||
|
||||
// 3. 현재 페이지 데이터 슬라이싱
|
||||
// 3. 현재 페이지 데이터 계산
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
||||
|
||||
if (state.viewMode === 'grid') {
|
||||
grid.innerHTML = '';
|
||||
pagedProducts.forEach((product) => {
|
||||
const isSold = STATUS_META[product.status]?.soldOut === true;
|
||||
grid.insertAdjacentHTML('beforeend', `
|
||||
<div class="group flex flex-col gap-4 cursor-pointer" onclick="openModal('${product.id}')">
|
||||
<div class="relative w-full aspect-card bg-slate-50 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm transition-shadow">
|
||||
<div class="w-full h-full bg-center bg-no-repeat bg-cover transform ${isSold ? 'grayscale opacity-80' : 'group-hover:scale-105'} transition-transform duration-500"
|
||||
style="background-image: url('${product.images[0]}')"></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>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start 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-slate-900 dark:text-white text-base font-bold text-nowrap">${product.currency}${product.price.toLocaleString()}</p>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1">${product.description}</p>
|
||||
</div>
|
||||
// 1. 상태 판별
|
||||
const isSold = STATUS_META[product.status]?.soldOut === true;
|
||||
const isNonSale = product.status === '미판매'; // 상태값이 '미판매'일 때
|
||||
|
||||
// 2. 스펙(Condition) 정보 추출
|
||||
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="window.openModal('${product.id}')"
|
||||
${!isSold ? `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 transform ${isSold ? 'grayscale opacity-60' : 'group-hover:scale-105 transition-transform duration-500'}"
|
||||
style="background-image: none; will-change: background-image;">
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
} else {
|
||||
// [테이블 렌더링: 스타일 & 체크박스 제어 추가]
|
||||
tableBody.innerHTML = pagedProducts
|
||||
.map((product) => {
|
||||
const meta = STATUS_META[product.status];
|
||||
const isSold = meta?.soldOut === true;
|
||||
const isSelectable = meta?.selectable !== false;
|
||||
|
||||
// 1. 상태(Condition) 설정 로직
|
||||
const conditionKey = product.specs.condition;
|
||||
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
|
||||
let conditionDisplay = '';
|
||||
let conditionClass = 'text-slate-500';
|
||||
|
||||
if (conditionConfig) {
|
||||
conditionDisplay = conditionConfig.label;
|
||||
conditionClass = conditionConfig.color;
|
||||
} else if (conditionKey && conditionKey.trim() !== '') {
|
||||
conditionDisplay = conditionKey;
|
||||
} else {
|
||||
conditionDisplay = '상세 설명 참고 ℹ️';
|
||||
conditionClass = 'text-indigo-500 italic';
|
||||
<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>
|
||||
|
||||
<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 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 });
|
||||
|
||||
// 2. 행 전체 스타일 및 제목 스타일
|
||||
const rowClass = isSold ? 'opacity-50 grayscale-[0.5]' : '';
|
||||
const titleClass = isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white';
|
||||
document.querySelectorAll('.product-card').forEach(card => observer.observe(card));
|
||||
|
||||
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('판매 완료된 상품은 상세 정보를 볼 수 없습니다.');" : `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('');
|
||||
} 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('');
|
||||
}
|
||||
|
||||
// [중요] 전체 선택 체크박스 상태 동기화
|
||||
// 전체 선택 체크박스 상태 동기화
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
renderPagination();
|
||||
// 페이지네이션 함수 호출 (이 함수는 외부에 정의되어 있어야 함)
|
||||
if (typeof renderPagination === 'function') {
|
||||
renderPagination();
|
||||
}
|
||||
}
|
||||
|
||||
export function renderPagination() {
|
||||
|
||||
Reference in New Issue
Block a user