[260219] 카드 터치 이벤트 수정

This commit is contained in:
2026-02-19 23:45:42 +09:00
parent cfd3fb8b75
commit 771f3fc9f2
15 changed files with 286 additions and 101 deletions

View File

@@ -1,4 +1,118 @@
const games = [ const games = [
{
id: 'r7c2v9km',
createdAt: '2026-02-18',
updatedAt: '2026-02-18',
title: '한계돌파 모에로 크리스탈 H',
price: 75000,
currency: '₩',
category: 'Games',
status: '판매중',
customTag: '',
tags: ['Switch', 'KR'],
images: ['/images/games/r7c2v9km_01.jpg', '/images/games/r7c2v9km_02.jpg', '/images/games/r7c2v9km_03.jpg'],
description: '개봉품, 한국 정발판, OPP 보관',
specs: {
purchaseDate: '',
condition: 'EXCELLENT',
isVerified: true,
},
fullDescription: ['한계돌파 모에로 크리스탈 H (限界凸起 モエロクリスタル ) 닌텐도 스위치용 패키지입니다.', '한국 정식 발매판 제품입니다.', '', '개봉 후 OPP에 넣어 보관·관리했으며', '전체적으로 상태가 매우 좋은 편입니다.', '', '', ''],
},
{
id: 'q8m3r5yk',
createdAt: '2026-02-18',
updatedAt: '2026-02-18',
title: '페이퍼 마리오 종이접기 킹',
price: 40000,
currency: '₩',
category: 'Games',
status: '판매중',
customTag: '',
tags: ['Switch', 'JP'],
images: ['/images/games/q8m3r5yk_01.jpg', '/images/games/q8m3r5yk_02.jpg', '/images/games/q8m3r5yk_03.jpg'],
description: '개봉품, 일본판, 한국어 지원, OPP 보관',
specs: {
purchaseDate: '',
condition: 'EXCELLENT',
isVerified: true,
},
fullDescription: ['페이퍼 마리오 종이접기 킹 (ペーパーマリオオリガミキング) 닌텐도 스위치용 패키지입니다.', '일본판 제품이며 한국어를 지원합니다.', '', '개봉 후 OPP에 넣어 보관·관리했으며', '전체적으로 상태가 매우 좋은 편입니다.', '', '', ''],
},
{
id: 'k7p2x9qa',
createdAt: '2026-02-18',
updatedAt: '2026-02-18',
title: '슈퍼 마리오 3D 컬렉션',
price: 140000,
currency: '₩',
category: 'Games',
status: '판매중',
customTag: '',
tags: ['Switch', 'JP'],
images: ['/images/games/k7p2x9qa_01.jpg', '/images/games/k7p2x9qa_02.jpg', '/images/games/k7p2x9qa_03.jpg'],
description: '미개봉 새제품, 일본판, 한국어 미지원',
specs: {
purchaseDate: '',
condition: 'BRAND_NEW',
isVerified: true,
},
fullDescription: [
'슈퍼 마리오 3D 컬렉션 (スーパーマリオ 3Dコレクション) 닌텐도 스위치용 패키지입니다.',
'일본판 미개봉 새제품입니다.',
'',
'수록 작품:',
'・『スーパーマリオ64』(1996년 / Nintendo 64)',
'・『スーパーマリオサンシャイン』(2002년 / 게임큐브)',
'・『スーパーマリオギャラクシー』(2007년 / Wii)',
'',
'지원 언어: 일본어, 영어, 프랑스어, 독일어',
],
},
{
id: 'k8d2m4qs',
createdAt: '2026-02-18',
updatedAt: '2026-02-18',
title: '사무라이 메이든',
price: 40000,
currency: '₩',
category: 'Games',
status: '판매중',
customTag: '',
tags: ['Switch', 'JP'],
images: ['/images/games/k8d2m4qs_01.jpg', '/images/games/k8d2m4qs_02.jpg', '/images/games/k8d2m4qs_03.jpg'],
description: '개봉 후 OPP 보관, 일본판(JP), 한국어 지원',
specs: {
purchaseDate: '',
condition: 'EXCELLENT',
isVerified: true,
},
fullDescription: ['사무라이 메이든 (SAMURAI MAIDEN -サムライメイデン-) 닌텐도 스위치용 패키지입니다.', '일본판(JP) 버전이며 한국어를 지원합니다.', '개봉 후 OPP에 넣어 보관했으며 상태는 매우 좋습니다.', '', '', '', '', ''],
},
{ {
id: 'm8q2v7kx', id: 'm8q2v7kx',
createdAt: '2026-02-16', createdAt: '2026-02-16',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -3,6 +3,90 @@ import { state, saveSelection } from './state.js';
import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js'; import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.js';
import { updateSummary } from './main.js'; import { updateSummary } from './main.js';
// --- 터치 및 드래그 관련 전역 변수 및 핸들러 ---
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 상태 유지만으로 충분)
};
// 썸네일 호버 핸들러 (PC에서만 동작하도록 수정)
window.handleThumbnailHover = function (e, id) {
if (isTouchDevice) return; // 모바일에서는 호버 로직 실행 안 함
const product = state.visibleProducts.find((p) => p.id === id);
if (!product || !product.images || product.images.length <= 1) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const sectionWidth = rect.width / product.images.length;
const index = Math.floor(x / sectionWidth);
const thumb = document.getElementById(`thumb-${id}`);
const indicators = document.querySelector(`#indicator-${id}`)?.children;
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');
}
});
}
};
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 (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');
}
});
}
};
// 1. 체크박스 전역 핸들러 등록 // 1. 체크박스 전역 핸들러 등록
window.toggleSelectItem = function (id) { window.toggleSelectItem = function (id) {
if (state.selectedIds.has(id)) { if (state.selectedIds.has(id)) {
@@ -24,32 +108,24 @@ export function renderProducts(page = 1) {
if (!grid || !tableWrapper) return; if (!grid || !tableWrapper) return;
// 1. 결과가 0개인 경우 안내
if (state.visibleProducts.length === 0) { if (state.visibleProducts.length === 0) {
grid.classList.remove('grid'); grid.classList.remove('grid');
grid.classList.add('hidden'); grid.classList.add('hidden');
tableWrapper.classList.add('hidden'); tableWrapper.classList.add('hidden');
// 검색 결과 없음 메시지를 표시할 별도의 컨테이너가 없다면 grid 영역을 빌려 씁니다.
const emptyMsg = ` const emptyMsg = `
<div class="col-span-full flex flex-col items-center justify-center py-20 w-full text-center"> <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" <svg class="w-20 h-20 text-slate-300 dark:text-slate-700 mb-6" viewBox="0 -960 960 960" fill="currentColor">
viewBox="0 -960 960 960"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<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"/> <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> </svg>
<h3 class="text-xl font-bold text-slate-900 dark:text-white mb-2">검색 결과가 없습니다</h3> <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> <p class="text-slate-500 dark:text-slate-400 text-sm">입력하신 검색어나 선택한 필터를 확인해 주세요.</p>
</div> </div>`;
`;
grid.innerHTML = emptyMsg; grid.innerHTML = emptyMsg;
grid.classList.remove('hidden'); grid.classList.remove('hidden');
if (paginationContainer) paginationContainer.innerHTML = ''; if (paginationContainer) paginationContainer.innerHTML = '';
return; return;
} }
// 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');
@@ -62,21 +138,17 @@ export function renderProducts(page = 1) {
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();
} }
// 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) => {
// 1. 상태 판별
const isSold = STATUS_META[product.status]?.soldOut === true; const isSold = STATUS_META[product.status]?.soldOut === true;
const isNonSale = product.status === '미판매'; // 상태값이 '미판매'일 때 const isNonSale = product.status === '미판매';
// 2. 스펙(Condition) 정보 추출
const conditionKey = product.specs?.condition; const conditionKey = product.specs?.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey]; const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
const conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || ''; const conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || '';
@@ -84,111 +156,95 @@ export function renderProducts(page = 1) {
grid.insertAdjacentHTML( grid.insertAdjacentHTML(
'beforeend', 'beforeend',
` `
<div <div class="product-card group flex flex-col gap-4 cursor-pointer"
class="product-card group flex flex-col gap-4 cursor-pointer" data-id="${product.id}"
data-id="${product.id}" onclick="if(!window.isDragging) window.openModal('${product.id}')"
onclick="if(!window.isDragging) window.openModal('${product.id}')" ontouchstart="window.handleTouchStart(event)"
${ ontouchmove="window.handleTouchMove(event)"
!isSold ontouchend="window.handleTouchEnd(event)"
? ` onmousemove="window.handleThumbnailHover(event, '${product.id}')"
onmousemove="window.handleThumbnailHover(event, '${product.id}')" onmouseleave="window.handleThumbnailLeave('${product.id}')">
onmouseleave="window.handleThumbnailLeave('${product.id}')"
ontouchstart="window.handleTouchStart(event)" <div class="relative w-full aspect-card bg-slate-200 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm">
ontouchmove="window.handleTouchMove(event, '${product.id}')" <div id="thumb-${product.id}"
ontouchend="window.handleTouchEnd(event, '${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;">
: `
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 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 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>
<div class="flex flex-col gap-1.5"> <div id="thumb-fade-${product.id}"
<div class="flex flex-col justify-between items-start sm:items-baseline gap-1"> 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'}"
<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> style="background-image: none; will-change: background-image, opacity, transform;">
<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>
<div class="flex flex-col"> <div class="absolute top-3 left-3 z-10">
${conditionDisplay ? `<span class="text-[11px] font-medium text-slate-400 mb-0.5">${conditionDisplay}</span>` : ''} <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>
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1 italic">${product.description}</p> </div>
</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> </div>
`, </div>`,
); );
}); });
setupLazyLoading(); setupLazyLoading();
} else { } else {
// 테이블 렌더링 // 테이블 렌더링 로직 (생략 없이 유지)
tableBody.innerHTML = pagedProducts tableBody.innerHTML = pagedProducts
.map((product) => { .map((product) => {
const meta = STATUS_META[product.status]; const meta = STATUS_META[product.status];
const isSold = meta?.soldOut === true; const isSold = meta?.soldOut === true;
const isSelectable = meta?.selectable !== false; const isSelectable = meta?.selectable !== false;
const conditionKey = product.specs.condition; const conditionKey = product.specs.condition;
const conditionConfig = PRODUCT_CONDITIONS[conditionKey]; const conditionConfig = PRODUCT_CONDITIONS[conditionKey];
let conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || '상세 설명 참고 '; let conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || '상세 설명 참고 ';
let conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500'; let conditionClass = conditionConfig ? conditionConfig.color : 'text-slate-500';
return ` return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors ${isSold ? 'opacity-50 grayscale cursor-not-allowed' : 'cursor-pointer'}" <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') { onclick="if(event.target.type !== 'checkbox') {
${isSold ? "alert('판매 완료된 상품입니다.');" : `window.openModal('${product.id}')`} ${isSold ? "alert('판매 완료된 상품입니다.');" : `window.openModal('${product.id}')`}
}"> }">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()"> <td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox" <input type="checkbox" class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer text-primary' : 'opacity-20 cursor-not-allowed'}"
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'}
${state.selectedIds.has(product.id) ? 'checked' : ''} onchange="window.toggleSelectItem('${product.id}')">
${isSelectable ? '' : 'disabled'} </td>
onchange="window.toggleSelectItem('${product.id}')"> <td class="py-4 px-4 font-semibold ${isSold ? 'line-through text-slate-400' : 'text-slate-900 dark:text-white'}">${product.title}</td>
</td> <td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</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-right font-bold text-slate-900 dark:text-white">${product.price.toLocaleString()}</td>
<td class="py-4 px-4 text-xs break-keep ${conditionClass}">${conditionDisplay}</td> <td class="hidden lg:table-cell py-4 px-4 text-center">
<td class="py-4 px-4 text-right font-bold text-slate-900 dark:text-white"> <span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
${product.price.toLocaleString()} </td>
</td> </tr>`;
<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(''); .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;
@@ -196,10 +252,7 @@ export function renderProducts(page = 1) {
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));
} }
// 페이지네이션 함수 호출 (이 함수는 외부에 정의되어 있어야 함) if (typeof renderPagination === 'function') renderPagination();
if (typeof renderPagination === 'function') {
renderPagination();
}
} }
function setupLazyLoading() { function setupLazyLoading() {

View File

@@ -37,6 +37,24 @@
button:disabled { button:disabled {
cursor: default; cursor: default;
} }
.product-card {
-webkit-user-select: none; /* Safari/Chrome/iOS 전용 */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
user-select: none; /* 표준 */
/* iOS에서 롱 터치 시 링크/이미지 미리보기 팝업이 뜨는 것 방지 */
-webkit-touch-callout: none;
}
/* 텍스트 입력창이나 모달 내부의 상세 설명 등은 선택이 가능해야 하므로 예외 처리 */
#product-modal,
input,
textarea {
-webkit-user-select: text;
user-select: text;
}
} }
@layer utilities { @layer utilities {