diff --git a/data/games.js b/data/games.js index b30a3b9..2f35da2 100644 --- a/data/games.js +++ b/data/games.js @@ -1,4 +1,118 @@ 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 (限界凸起 モエロクリスタル 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', createdAt: '2026-02-16', diff --git a/images/games/k7p2x9qa_01.jpg b/images/games/k7p2x9qa_01.jpg new file mode 100644 index 0000000..b444acf Binary files /dev/null and b/images/games/k7p2x9qa_01.jpg differ diff --git a/images/games/k7p2x9qa_02.jpg b/images/games/k7p2x9qa_02.jpg new file mode 100644 index 0000000..90e6d9f Binary files /dev/null and b/images/games/k7p2x9qa_02.jpg differ diff --git a/images/games/k7p2x9qa_03.jpg b/images/games/k7p2x9qa_03.jpg new file mode 100644 index 0000000..544227d Binary files /dev/null and b/images/games/k7p2x9qa_03.jpg differ diff --git a/images/games/k8d2m4qs_01.jpg b/images/games/k8d2m4qs_01.jpg new file mode 100644 index 0000000..39bddc7 Binary files /dev/null and b/images/games/k8d2m4qs_01.jpg differ diff --git a/images/games/k8d2m4qs_02.jpg b/images/games/k8d2m4qs_02.jpg new file mode 100644 index 0000000..8e7f378 Binary files /dev/null and b/images/games/k8d2m4qs_02.jpg differ diff --git a/images/games/k8d2m4qs_03.jpg b/images/games/k8d2m4qs_03.jpg new file mode 100644 index 0000000..6c46fd8 Binary files /dev/null and b/images/games/k8d2m4qs_03.jpg differ diff --git a/images/games/q8m3r5yk_01.jpg b/images/games/q8m3r5yk_01.jpg new file mode 100644 index 0000000..e1850e7 Binary files /dev/null and b/images/games/q8m3r5yk_01.jpg differ diff --git a/images/games/q8m3r5yk_02.jpg b/images/games/q8m3r5yk_02.jpg new file mode 100644 index 0000000..4fe7ae1 Binary files /dev/null and b/images/games/q8m3r5yk_02.jpg differ diff --git a/images/games/q8m3r5yk_03.jpg b/images/games/q8m3r5yk_03.jpg new file mode 100644 index 0000000..872d15e Binary files /dev/null and b/images/games/q8m3r5yk_03.jpg differ diff --git a/images/games/r7c2v9km_01.jpg b/images/games/r7c2v9km_01.jpg new file mode 100644 index 0000000..d260a9d Binary files /dev/null and b/images/games/r7c2v9km_01.jpg differ diff --git a/images/games/r7c2v9km_02.jpg b/images/games/r7c2v9km_02.jpg new file mode 100644 index 0000000..4d651c4 Binary files /dev/null and b/images/games/r7c2v9km_02.jpg differ diff --git a/images/games/r7c2v9km_03.jpg b/images/games/r7c2v9km_03.jpg new file mode 100644 index 0000000..a8f936e Binary files /dev/null and b/images/games/r7c2v9km_03.jpg differ diff --git a/scripts/productList.js b/scripts/productList.js index b2cee8c..ebd9106 100644 --- a/scripts/productList.js +++ b/scripts/productList.js @@ -3,6 +3,90 @@ import { state, saveSelection } from './state.js'; import { ITEMS_PER_PAGE, STATUS_META, STATUS_COLOR, PRODUCT_CONDITIONS } from './config.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. 체크박스 전역 핸들러 등록 window.toggleSelectItem = function (id) { if (state.selectedIds.has(id)) { @@ -24,32 +108,24 @@ export function renderProducts(page = 1) { if (!grid || !tableWrapper) return; - // 1. 결과가 0개인 경우 안내 if (state.visibleProducts.length === 0) { grid.classList.remove('grid'); grid.classList.add('hidden'); tableWrapper.classList.add('hidden'); - - // 검색 결과 없음 메시지를 표시할 별도의 컨테이너가 없다면 grid 영역을 빌려 씁니다. const emptyMsg = `
- +

검색 결과가 없습니다

입력하신 검색어나 선택한 필터를 확인해 주세요.

-
- `; + `; grid.innerHTML = emptyMsg; grid.classList.remove('hidden'); if (paginationContainer) paginationContainer.innerHTML = ''; return; } - // 2. 뷰 모드 설정 및 컨테이너 노출 정리 (hidden/flex/grid 충돌 방지) if (state.viewMode === 'grid') { grid.classList.remove('hidden'); grid.classList.add('grid'); @@ -62,21 +138,17 @@ export function renderProducts(page = 1) { grid.classList.remove('grid'); grid.classList.add('hidden'); tableWrapper.classList.remove('hidden'); - updateSummary(); // 테이블일 때만 요약바 노출 여부 결정 + updateSummary(); } - // 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) => { - // 1. 상태 판별 const isSold = STATUS_META[product.status]?.soldOut === true; - const isNonSale = product.status === '미판매'; // 상태값이 '미판매'일 때 - - // 2. 스펙(Condition) 정보 추출 + const isNonSale = product.status === '미판매'; const conditionKey = product.specs?.condition; const conditionConfig = PRODUCT_CONDITIONS[conditionKey]; const conditionDisplay = conditionConfig ? conditionConfig.label : conditionKey || ''; @@ -84,111 +156,95 @@ export function renderProducts(page = 1) { grid.insertAdjacentHTML( 'beforeend', ` -
-
-
-
- -
-
- -
- ${product.status} -
- - ${ - !isSold && product.images?.length > 1 - ? ` -
- ${product.images - .map( - (_, i) => ` -
- `, - ) - .join('')} -
- ` - : '' - } +
+ +
+
-
-
-

${product.title}

-

${isNonSale ? 'Not for Sale' : `${product.currency || '₩'}${product.price.toLocaleString()}`}

-
+
+
-
- ${conditionDisplay ? `${conditionDisplay}` : ''} -

${product.description}

-
+
+ ${product.status} +
+ + ${ + !isSold && product.images?.length > 1 + ? ` + ` + : '' + } +
+ +
+
+

${product.title}

+

+ ${isNonSale ? 'Not for Sale' : `${product.currency || '₩'}${product.price.toLocaleString()}`} +

+
+
+ ${conditionDisplay ? `${conditionDisplay}` : ''} +

${product.description}

- `, +
`, ); }); - setupLazyLoading(); } 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 ` - - - - - ${product.title} - ${conditionDisplay} - - ₩${product.price.toLocaleString()} - - - ${product.status} - - `; + + + + + ${product.title} + ${conditionDisplay} + ₩${product.price.toLocaleString()} + + ${product.status} + + `; }) .join(''); } - // 전체 선택 체크박스 상태 동기화 const selectAllCheck = document.getElementById('select-all-current'); if (selectAllCheck) { 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)); } - // 페이지네이션 함수 호출 (이 함수는 외부에 정의되어 있어야 함) - if (typeof renderPagination === 'function') { - renderPagination(); - } + if (typeof renderPagination === 'function') renderPagination(); } function setupLazyLoading() { diff --git a/style/input.css b/style/input.css index a5901cd..f1fdb3a 100644 --- a/style/input.css +++ b/style/input.css @@ -37,6 +37,24 @@ button:disabled { 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 {