[260211]
- 썸네일 슬라이드 기능 추가 - 미판매 상품 가격 표시 안함 추후 해야할일: 이미지 webp로 변환
This commit is contained in:
@@ -5,7 +5,7 @@ const games = [
|
|||||||
updatedAt: '2026-02-11',
|
updatedAt: '2026-02-11',
|
||||||
|
|
||||||
title: '-KATANA Project CompleteBox- 煌花絢爛',
|
title: '-KATANA Project CompleteBox- 煌花絢爛',
|
||||||
price: 0,
|
price: 250000,
|
||||||
currency: '₩',
|
currency: '₩',
|
||||||
category: 'PC Games',
|
category: 'PC Games',
|
||||||
status: '미판매',
|
status: '미판매',
|
||||||
@@ -88,7 +88,7 @@ const games = [
|
|||||||
price: 60000,
|
price: 60000,
|
||||||
currency: '₩',
|
currency: '₩',
|
||||||
category: 'Games',
|
category: 'Games',
|
||||||
status: '판매완료',
|
status: '판매중',
|
||||||
customTag: '',
|
customTag: '',
|
||||||
|
|
||||||
tags: ['Switch', 'JP'],
|
tags: ['Switch', 'JP'],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const STATUS_META = {
|
|||||||
미판매: {
|
미판매: {
|
||||||
selectable: false, // 체크박스 선택 불가
|
selectable: false, // 체크박스 선택 불가
|
||||||
isDefaultActive: false, // 초기 로드 시 미체크 상태
|
isDefaultActive: false, // 초기 로드 시 미체크 상태
|
||||||
isSystemVisible: false, // 아예 리스트/필터에서 제외 (완전 숨김)
|
isSystemVisible: true, // 아예 리스트/필터에서 제외 (완전 숨김)
|
||||||
soldOut: false,
|
soldOut: false,
|
||||||
},
|
},
|
||||||
판매예정: {
|
판매예정: {
|
||||||
@@ -30,7 +30,7 @@ export const STATUS_META = {
|
|||||||
selectable: false,
|
selectable: false,
|
||||||
isDefaultActive: false, // 초기에는 안 보이지만, 사용자가 필터 클릭하면 보임
|
isDefaultActive: false, // 초기에는 안 보이지만, 사용자가 필터 클릭하면 보임
|
||||||
isSystemVisible: true,
|
isSystemVisible: true,
|
||||||
soldOut: true, // 이미지에 SOLD OUT 표시
|
soldOut: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -256,3 +256,59 @@ window.exportToExcel = () => {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
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;
|
if (!grid || !tableWrapper) return;
|
||||||
|
|
||||||
// 1. 결과가 0개인 경우 (그리드/테이블 공통 안내)
|
// 1. 결과가 0개인 경우 안내
|
||||||
if (state.visibleProducts.length === 0) {
|
if (state.visibleProducts.length === 0) {
|
||||||
grid.classList.remove('grid', 'hidden');
|
grid.classList.remove('grid');
|
||||||
grid.innerHTML = `
|
grid.classList.add('hidden');
|
||||||
<div class="flex flex-col items-center justify-center py-20 w-full text-center">
|
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>
|
<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>
|
<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>
|
<p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
tableWrapper.classList.add('hidden');
|
grid.innerHTML = emptyMsg;
|
||||||
|
grid.classList.remove('hidden');
|
||||||
if (paginationContainer) paginationContainer.innerHTML = '';
|
if (paginationContainer) paginationContainer.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 뷰 모드에 따른 컨테이너 노출 설정
|
// 2. 뷰 모드 설정 및 컨테이너 노출 정리 (hidden/flex/grid 충돌 방지)
|
||||||
if (state.viewMode === 'grid') {
|
if (state.viewMode === 'grid') {
|
||||||
// 그리드 활성화
|
|
||||||
grid.classList.remove('hidden');
|
grid.classList.remove('hidden');
|
||||||
grid.classList.add('grid');
|
grid.classList.add('grid');
|
||||||
|
|
||||||
// 테이블 및 요약바 비활성화
|
|
||||||
tableWrapper.classList.add('hidden');
|
tableWrapper.classList.add('hidden');
|
||||||
if (summaryBar) {
|
if (summaryBar) {
|
||||||
summaryBar.classList.remove('flex'); // flex 제거
|
summaryBar.classList.remove('flex');
|
||||||
summaryBar.classList.add('hidden');
|
summaryBar.classList.add('hidden');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 그리드 비활성화
|
|
||||||
grid.classList.remove('grid');
|
grid.classList.remove('grid');
|
||||||
grid.classList.add('hidden');
|
grid.classList.add('hidden');
|
||||||
|
|
||||||
// 테이블 활성화
|
|
||||||
tableWrapper.classList.remove('hidden');
|
tableWrapper.classList.remove('hidden');
|
||||||
|
updateSummary(); // 테이블일 때만 요약바 노출 여부 결정
|
||||||
// 요약바 노출 여부는 데이터 상태에 따라 updateSummary에서 결정
|
|
||||||
updateSummary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 현재 페이지 데이터 슬라이싱
|
// 3. 현재 페이지 데이터 계산
|
||||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||||
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
const pagedProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
||||||
|
|
||||||
if (state.viewMode === 'grid') {
|
if (state.viewMode === 'grid') {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
pagedProducts.forEach((product) => {
|
pagedProducts.forEach((product) => {
|
||||||
const isSold = STATUS_META[product.status]?.soldOut === true;
|
// 1. 상태 판별
|
||||||
grid.insertAdjacentHTML('beforeend', `
|
const isSold = STATUS_META[product.status]?.soldOut === true;
|
||||||
<div class="group flex flex-col gap-4 cursor-pointer" onclick="openModal('${product.id}')">
|
const isNonSale = product.status === '미판매'; // 상태값이 '미판매'일 때
|
||||||
<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"
|
// 2. 스펙(Condition) 정보 추출
|
||||||
style="background-image: url('${product.images[0]}')"></div>
|
const conditionKey = product.specs?.condition;
|
||||||
<div class="absolute top-3 left-3">
|
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
|
||||||
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">
|
const conditionDisplay = conditionConfig ? conditionConfig.label : (conditionKey || '');
|
||||||
${product.status}
|
|
||||||
</span>
|
grid.insertAdjacentHTML('beforeend', `
|
||||||
</div>
|
<div class="product-card group flex flex-col gap-4 cursor-pointer"
|
||||||
</div>
|
data-id="${product.id}"
|
||||||
<div class="flex flex-col gap-1">
|
onclick="window.openModal('${product.id}')"
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-start gap-1">
|
${!isSold ? `onmousemove="window.handleThumbnailHover(event, '${product.id}')"
|
||||||
<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>
|
onmouseleave="window.handleThumbnailLeave('${product.id}')"` : ''}>
|
||||||
<p class="text-slate-900 dark:text-white text-base font-bold text-nowrap">${product.currency}${product.price.toLocaleString()}</p>
|
|
||||||
</div>
|
<div class="relative w-full aspect-card bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm">
|
||||||
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1">${product.description}</p>
|
<div id="thumb-${product.id}"
|
||||||
</div>
|
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>
|
||||||
`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// [테이블 렌더링: 스타일 & 체크박스 제어 추가]
|
|
||||||
tableBody.innerHTML = pagedProducts
|
|
||||||
.map((product) => {
|
|
||||||
const meta = STATUS_META[product.status];
|
|
||||||
const isSold = meta?.soldOut === true;
|
|
||||||
const isSelectable = meta?.selectable !== false;
|
|
||||||
|
|
||||||
// 1. 상태(Condition) 설정 로직
|
<div class="absolute top-3 left-3">
|
||||||
const conditionKey = product.specs.condition;
|
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">
|
||||||
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
|
${product.status}
|
||||||
let conditionDisplay = '';
|
</span>
|
||||||
let conditionClass = 'text-slate-500';
|
</div>
|
||||||
|
|
||||||
if (conditionConfig) {
|
${!isSold && product.images?.length > 1 ? `
|
||||||
conditionDisplay = conditionConfig.label;
|
<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">
|
||||||
conditionClass = conditionConfig.color;
|
${product.images.map((_, i) => `
|
||||||
} else if (conditionKey && conditionKey.trim() !== '') {
|
<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>
|
||||||
conditionDisplay = conditionKey;
|
`).join('')}
|
||||||
} else {
|
</div>
|
||||||
conditionDisplay = '상세 설명 참고 ℹ️';
|
` : ''}
|
||||||
conditionClass = 'text-indigo-500 italic';
|
</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. 행 전체 스타일 및 제목 스타일
|
document.querySelectorAll('.product-card').forEach(card => observer.observe(card));
|
||||||
const rowClass = isSold ? 'opacity-50 grayscale-[0.5]' : '';
|
|
||||||
const titleClass = isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white';
|
|
||||||
|
|
||||||
return `
|
} else {
|
||||||
<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') {
|
tableBody.innerHTML = pagedProducts.map((product) => {
|
||||||
${isSold ? "alert('판매 완료된 상품은 상세 정보를 볼 수 없습니다.');" : `openModal('${product.id}')`}
|
const meta = STATUS_META[product.status];
|
||||||
}">
|
const isSold = meta?.soldOut === true;
|
||||||
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
|
const isSelectable = meta?.selectable !== false;
|
||||||
<input type="checkbox"
|
|
||||||
class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
|
const conditionKey = product.specs.condition;
|
||||||
${state.selectedIds.has(product.id) ? 'checked' : ''}
|
const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
|
||||||
${isSelectable ? '' : 'disabled'}
|
let conditionDisplay = conditionConfig ? conditionConfig.label : (conditionKey || '상세 설명 참고 ℹ️');
|
||||||
onchange="window.toggleSelectItem('${product.id}')">
|
let conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
|
||||||
</td>
|
|
||||||
<td class="py-4 px-4 font-semibold ${isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white'}">${product.title}</td>
|
return `
|
||||||
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td>
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}"
|
||||||
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white">
|
onclick="if(event.target.type !== 'checkbox') {
|
||||||
₩${product.price.toLocaleString()}
|
${isSold ? "alert('판매 완료된 상품입니다.');" : `window.openModal('${product.id}')`}
|
||||||
</td>
|
}">
|
||||||
<td class="hidden lg:table-cell py-4 px-4 text-center">
|
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
|
||||||
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
|
<input type="checkbox"
|
||||||
</td>
|
class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
|
||||||
</tr>`;
|
${state.selectedIds.has(product.id) ? 'checked' : ''}
|
||||||
})
|
${isSelectable ? '' : 'disabled'}
|
||||||
.join('');
|
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');
|
const selectAllCheck = document.getElementById('select-all-current');
|
||||||
if (selectAllCheck) {
|
if (selectAllCheck) {
|
||||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
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));
|
selectAllCheck.checked = currentSelectableItems.length > 0 && currentSelectableItems.every((p) => state.selectedIds.has(p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPagination();
|
// 페이지네이션 함수 호출 (이 함수는 외부에 정의되어 있어야 함)
|
||||||
|
if (typeof renderPagination === 'function') {
|
||||||
|
renderPagination();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderPagination() {
|
export function renderPagination() {
|
||||||
|
|||||||
Reference in New Issue
Block a user