From cfd3fb8b757becdc6d418ca6a90555beeab8cdc9 Mon Sep 17 00:00:00 2001 From: zenn Date: Wed, 18 Feb 2026 03:35:55 +0900 Subject: [PATCH] =?UTF-8?q?[260218]=20-=20=EC=B9=B4=EB=93=9C=20=EA=B9=9C?= =?UTF-8?q?=EB=B9=A1=EC=9E=84=20=EB=B3=B4=EC=99=84=20-=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=83=81=ED=83=9C,=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=A9=EB=B2=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/games.js | 2 +- index.html | 10 ++++- scripts/main.js | 72 +++++++++++++++++++---------------- scripts/modal.js | 58 ++++++++++++++++++----------- style/input.css | 21 ++++++++++- style/tailwind.css | 93 +++++++--------------------------------------- 6 files changed, 119 insertions(+), 137 deletions(-) diff --git a/data/games.js b/data/games.js index 11e76e4..b30a3b9 100644 --- a/data/games.js +++ b/data/games.js @@ -91,7 +91,7 @@ const games = [ tags: ['JP'], - images: ['/images/games/m7q4z8kt_01.jpg', '/images/games/m7q4z8kt_02.jpg', '/images/games/m7q4z8kt_03.jpg', '/images/games/m7q4z8kt_04.jpg'], + images: ['/images/games/m7q4z8kt_01.jpg', '/images/games/m7q4z8kt_02.jpg', '/images/games/m7q4z8kt_03.jpg'], description: '개봉 후 OPP 보관, 일본 내수용(JP), 한국어 미지원', diff --git a/index.html b/index.html index 60f5445..d5373cd 100644 --- a/index.html +++ b/index.html @@ -248,9 +248,9 @@ -

Sony WH-1000XM5 Noise Canceling Headphones

+

product

- +
+
+ 판매가 + +
diff --git a/scripts/main.js b/scripts/main.js index faf84e0..afcacba 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -8,6 +8,8 @@ import { scrollToImage } from './carousel.js'; console.log('Total products loaded:', productsData.length); +let lastThumbnailIndex = -1; + // HTML onclick에서 사용하기 위한 전역 등록 window.openModal = openModal; window.closeModal = closeModal; @@ -326,31 +328,32 @@ window.handleThumbnailHover = (e, productId) => { } }; -// 페이드 업데이트 함수 +/** * 썸네일을 즉시 업데이트하는 함수 + * 페이드 애니메이션을 제거하여 반응성을 높이고 깜빡임을 방지합니다. + */ function updateThumbnailWithFade(productId, newImageUrl, index) { const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); const indicator = document.getElementById(`indicator-${productId}`); - if (!mainThumb || !fadeThumb) return; + if (!mainThumb) return; - // 기존에 해당 카드에서 돌아가던 타이머가 있다면 즉시 제거 + // 1. 기존 페이드 타이머가 있다면 즉시 제거 (충돌 방지) if (fadeTimers[productId]) { clearTimeout(fadeTimers[productId]); + delete fadeTimers[productId]; } - // 페이드 레이어 세팅 - fadeThumb.style.transition = 'opacity 0.3s ease-in-out'; - fadeThumb.style.backgroundImage = `url("${newImageUrl}")`; - fadeThumb.style.opacity = '1'; - - // 타이머 시작 - fadeTimers[productId] = setTimeout(() => { - mainThumb.style.backgroundImage = `url("${newImageUrl}")`; + // 2. 페이드 레이어(뒷배경)는 즉시 숨기고 메인 이미지만 즉시 교체 + // transition 없이 즉시 교체되도록 인라인 스타일로 제어합니다. + if (fadeThumb) { + fadeThumb.style.transition = 'none'; fadeThumb.style.opacity = '0'; - delete fadeTimers[productId]; // 작업 완료 후 타이머 삭제 - }, 300); + } + + mainThumb.style.backgroundImage = `url("${newImageUrl}")`; + // 3. 인디케이터 UI 업데이트 if (indicator) updateIndicatorUI(indicator, index); } @@ -360,8 +363,9 @@ window.handleThumbnailLeave = (productId) => { resetThumbnail(productId); }; +/** * 마우스가 나갔을 때 썸네일을 첫 번째 이미지로 복구하는 함수 + */ function resetThumbnail(productId) { - // 1. 진행 중인 모든 페이드 타이머 즉시 파괴 if (fadeTimers[productId]) { clearTimeout(fadeTimers[productId]); delete fadeTimers[productId]; @@ -374,21 +378,17 @@ function resetThumbnail(productId) { const fadeThumb = document.getElementById(`thumb-fade-${productId}`); const indicator = document.getElementById(`indicator-${productId}`); - if (mainThumb && fadeThumb) { - const firstImg = `url("${product.images[0]}")`; - - // 2. 페이드 레이어를 즉시 숨김 (transition 방해 금지) - fadeThumb.style.transition = 'none'; - fadeThumb.style.opacity = '0'; - - // 3. 두 레이어 모두 첫 번째 이미지로 강제 일치 - mainThumb.style.backgroundImage = firstImg; - fadeThumb.style.backgroundImage = firstImg; - - // 4. 다음 호버를 위해 트랜지션 복구 - setTimeout(() => { - fadeThumb.style.transition = 'opacity 0.3s ease-in-out'; - }, 50); + if (mainThumb) { + const firstImgUrl = `url("${product.images[0]}")`; + + // 즉시 첫 번째 이미지로 복구 + mainThumb.style.backgroundImage = firstImgUrl; + + if (fadeThumb) { + fadeThumb.style.transition = 'none'; + fadeThumb.style.opacity = '0'; + fadeThumb.style.backgroundImage = firstImgUrl; + } } if (indicator) updateIndicatorUI(indicator, 0); @@ -428,17 +428,24 @@ window.handleTouchMove = (e, productId) => { } if (isDragging) { + const product = productsData.find((p) => p.id === productId); const step = cardWidth / product.images.length; let index = Math.floor(Math.abs(diffX) / step); index = Math.max(0, Math.min(product.images.length - 1, index)); + // [수정 핵심] 인덱스가 이전과 같으면 함수를 종료하여 불필요한 리렌더링 방지 + if (lastThumbnailIndex === index) return; + lastThumbnailIndex = index; + const mainThumb = document.getElementById(`thumb-${productId}`); const fadeThumb = document.getElementById(`thumb-fade-${productId}`); - if (mainThumb && fadeThumb) { - // 드래그 중에는 페이드 없이 즉시 교체 (반응성 우선) + // [수정 핵심] 인덱스가 실제로 변했을 때만 스타일을 바꿉니다. + if (mainThumb && lastThumbnailIndex !== index) { + lastThumbnailIndex = index; // 새 인덱스 저장 + mainThumb.style.backgroundImage = `url("${product.images[index]}")`; - fadeThumb.style.opacity = '0'; // 페이드 레이어 숨김 + if (fadeThumb) fadeThumb.style.opacity = '0'; updateIndicator(productId, index); } } @@ -452,6 +459,7 @@ window.handleTouchEnd = (e, productId) => { } else { resetThumbnail(productId); // 드래그 종료 시 확실한 리셋 } + lastThumbnailIndex = -1; // 초기화 isDragging = false; }; diff --git a/scripts/modal.js b/scripts/modal.js index 7d5a196..886c36a 100644 --- a/scripts/modal.js +++ b/scripts/modal.js @@ -1,7 +1,7 @@ /** 상품 상세 모달 (열기/닫기·콘텐츠 채우기·링크 복사) */ import { productsData } from './state.js'; import { initBetterCarousel } from './carousel.js'; -import { TAG_STYLES, TAG_DEFAULT_STYLE } from './config.js'; +import { TAG_STYLES, TAG_DEFAULT_STYLE, PRODUCT_CONDITIONS } from './config.js'; export function openModal(id) { const product = productsData.find((p) => p.id === id); @@ -11,7 +11,7 @@ export function openModal(id) { modal.classList.remove('hidden'); modal.classList.add('flex'); - + const images = product.images; const loopImages = [images[images.length - 1], ...images, images[0]]; @@ -51,7 +51,6 @@ export function openModal(id) { document.getElementById('modal-thumbnails').innerHTML = thumbnailsHtml; document.getElementById('modal-dots').innerHTML = dotsHtml; document.getElementById('modal-title').textContent = product.title; - document.getElementById('modal-price').textContent = `${product.currency}${product.price.toLocaleString()}`; const modalCategory = document.getElementById('modal-category'); if (modalCategory) modalCategory.textContent = product.category; @@ -95,17 +94,21 @@ export function openModal(id) { } } - - // 제품 상태(specs.condition): 값이 있을 때만 행 노출 - const conditionText = product.specs?.condition; + const conditionKey = product.specs?.condition; const isVerified = product.specs?.isVerified; + + // PRODUCT_CONDITIONS에서 라벨을 가져오되, 없으면 원본 키 표시 + const conditionData = PRODUCT_CONDITIONS[conditionKey]; + // 데이터가 객체라면 .label을 쓰고, 아니면 원본 키를 씁니다. + const conditionLabel = conditionData?.label || conditionKey || ''; + const conditionValueEl = document.getElementById('modal-condition'); const conditionRowEl = document.getElementById('modal-condition-row'); const conditionRowWrap = conditionRowEl?.parentElement; // 라벨+값 전체 행 const verifiedIcon = document.getElementById('modal-verified-icon'); - if (conditionText && String(conditionText).trim() !== '') { - if (conditionValueEl) conditionValueEl.textContent = conditionText; + if (conditionKey && String(conditionKey).trim() !== '') { + if (conditionValueEl) conditionValueEl.textContent = conditionLabel; if (conditionRowWrap) { conditionRowWrap.classList.remove('hidden'); conditionRowWrap.classList.add('flex'); @@ -121,17 +124,30 @@ export function openModal(id) { } } -// 가격 표시 로직 수정 - const priceElement = document.getElementById('modal-price'); - if (priceElement) { + // 가격 표시 로직 수정 + const priceValueEl = document.getElementById('modal-price'); + const priceRowEl = document.getElementById('modal-price-row'); + const priceRowWrap = priceRowEl?.parentElement; + + if (priceValueEl && priceRowWrap) { + // [중요] 새로운 상품을 열 때마다 일단 hidden을 제거하여 초기화합니다. + priceRowWrap.classList.remove('hidden'); + + // 1. 최우선 순위: 미판매 상태 체크 if (product.status === '미판매') { - // 미판매 상태일 때의 처리 - priceElement.textContent = 'NOT FOR SALE'; - priceElement.classList.add('text-gray-500'); // 시각적으로 구분되도록 스타일 추가 가능 - } else { - // 정상 판매 중인 경우 - priceElement.textContent = `${product.currency}${product.price.toLocaleString()}`; - priceElement.classList.remove('text-gray-500'); + priceValueEl.textContent = 'NOT FOR SALE'; + priceValueEl.classList.add('text-gray-500'); + } + // 2. 가격 데이터가 아예 없는 경우 (null, undefined, 빈 문자열) + // 숫자 0은 가격으로 인정하고 싶다면 (product.price === null || product.price === undefined)로 씁니다. + else if (product.price === null || product.price === undefined || product.price === '') { + priceRowWrap.classList.add('hidden'); + } + // 3. 가격이 존재하는 경우 (0원을 포함하여 값이 있는 경우) + else { + const currency = product.currency || '₩'; + priceValueEl.textContent = `${currency}${Number(product.price).toLocaleString()}`; + priceValueEl.classList.remove('text-gray-500'); } } @@ -141,9 +157,7 @@ export function openModal(id) { if (product.status === '미판매') { modalDesc.innerHTML = '

판매중인 상품이 아니기에 정보가 제공되지 않습니다.

'; } else { - modalDesc.innerHTML = Array.isArray(product.fullDescription) - ? product.fullDescription.join('
') - : product.fullDescription || ''; + modalDesc.innerHTML = Array.isArray(product.fullDescription) ? product.fullDescription.join('
') : product.fullDescription || ''; } } @@ -178,7 +192,7 @@ export function openModal(id) { export function closeModal() { const modal = document.getElementById('product-modal'); - + // 1. 이미지 캐러셀 영역 초기화 const carouselContainer = document.getElementById('modal-main-carousel-container'); if (carouselContainer) { diff --git a/style/input.css b/style/input.css index e6aeba0..a5901cd 100644 --- a/style/input.css +++ b/style/input.css @@ -63,8 +63,27 @@ overscroll-behavior: contain; } [id^='thumb-'] { - transition: all 0.6s ease-in-out; + /* background-image는 0s로 즉시 교체, transform(확대)만 부드럽게 */ + transition: + transform 0.4s ease-out, + background-image 0s !important; + background-color: #f1f5f9; + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + + aspect-ratio: 1 / 1; + overflow: hidden; + will-change: transform; + + /* 기본 상태 */ + transform: scale(1); + } + + /* 카드에 호버 시 확대 상태 고정 */ + .group:hover [id^='thumb-'] { + transform: scale(1.1) !important; } } diff --git a/style/tailwind.css b/style/tailwind.css index 5c40cf8..da713d1 100644 --- a/style/tailwind.css +++ b/style/tailwind.css @@ -104,8 +104,6 @@ --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); - --text-5xl: 3rem; - --text-5xl--line-height: 1; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; @@ -120,8 +118,6 @@ --radius-lg: 0.5rem; --radius-xl: 0.75rem; --radius-2xl: 1rem; - --ease-in: cubic-bezier(0.4, 0, 1, 1); - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --blur-sm: 8px; --blur-md: 12px; --default-transition-duration: 150ms; @@ -131,7 +127,6 @@ --color-background-light: var(--bg-light); --color-background-dark: var(--bg-dark); --color-primary: #137fec; - --font-display: 'Inter', sans-serif; --aspect-card: 4 / 5; } } @@ -389,21 +384,12 @@ .mt-2 { margin-top: calc(var(--spacing) * 2); } - .mt-4 { - margin-top: calc(var(--spacing) * 4); - } - .mt-6 { - margin-top: calc(var(--spacing) * 6); - } .-mb-4 { margin-bottom: calc(var(--spacing) * -4); } .mb-0\.5 { margin-bottom: calc(var(--spacing) * 0.5); } - .mb-1 { - margin-bottom: calc(var(--spacing) * 1); - } .mb-2 { margin-bottom: calc(var(--spacing) * 2); } @@ -620,9 +606,6 @@ .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .snap-center { scroll-snap-align: center; } @@ -1013,9 +996,6 @@ .py-4 { padding-block: calc(var(--spacing) * 4); } - .py-6 { - padding-block: calc(var(--spacing) * 6); - } .py-8 { padding-block: calc(var(--spacing) * 8); } @@ -1052,17 +1032,6 @@ .text-right { text-align: right; } - .font-display { - font-family: var(--font-display); - } - .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - } - .text-3xl { - font-size: var(--text-3xl); - line-height: var(--tw-leading, var(--text-3xl--line-height)); - } .text-base { font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); @@ -1166,9 +1135,6 @@ .text-emerald-700 { color: var(--color-emerald-700); } - .text-gray-300 { - color: var(--color-gray-300); - } .text-gray-400 { color: var(--color-gray-400); } @@ -1307,17 +1273,10 @@ --tw-ring-offset-width: 1px; --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } .grayscale { --tw-grayscale: grayscale(100%); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } .backdrop-blur-md { --tw-backdrop-blur: blur(var(--blur-md)); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); @@ -1328,10 +1287,6 @@ -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } - .backdrop-filter { - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1364,14 +1319,6 @@ --tw-duration: 500ms; transition-duration: 500ms; } - .ease-in { - --tw-ease: var(--ease-in); - transition-timing-function: var(--ease-in); - } - .ease-in-out { - --tw-ease: var(--ease-in-out); - transition-timing-function: var(--ease-in-out); - } .select-none { -webkit-user-select: none; user-select: none; @@ -1561,11 +1508,6 @@ gap: calc(var(--spacing) * 2); } } - .sm\:gap-12 { - @media (width >= 40rem) { - gap: calc(var(--spacing) * 12); - } - } .sm\:rounded-lg { @media (width >= 40rem) { border-radius: var(--radius-lg); @@ -1607,18 +1549,6 @@ line-height: var(--tw-leading, var(--text-2xl--line-height)); } } - .sm\:text-4xl { - @media (width >= 40rem) { - font-size: var(--text-4xl); - line-height: var(--tw-leading, var(--text-4xl--line-height)); - } - } - .sm\:text-5xl { - @media (width >= 40rem) { - font-size: var(--text-5xl); - line-height: var(--tw-leading, var(--text-5xl--line-height)); - } - } .sm\:text-base { @media (width >= 40rem) { font-size: var(--text-base); @@ -2178,8 +2108,18 @@ overscroll-behavior: contain; } [id^='thumb-'] { - transition: all 0.6s ease-in-out; + transition: transform 0.4s ease-out, background-image 0s !important; background-color: #f1f5f9; + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + aspect-ratio: 1 / 1; + overflow: hidden; + will-change: transform; + transform: scale(1); + } + .group:hover [id^='thumb-'] { + transform: scale(1.1) !important; } } @property --tw-translate-x { @@ -2324,11 +2264,6 @@ inherits: false; initial-value: 0 0 #0000; } -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} @property --tw-blur { syntax: "*"; inherits: false; @@ -2422,9 +2357,10 @@ syntax: "*"; inherits: false; } -@property --tw-ease { +@property --tw-outline-style { syntax: "*"; inherits: false; + initial-value: solid; } @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { @@ -2460,7 +2396,6 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; - --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; @@ -2484,7 +2419,7 @@ --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; --tw-duration: initial; - --tw-ease: initial; + --tw-outline-style: solid; } } }