From eaa8792c18700ac173dbe57058f2b2f2dc028672 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 22:51:27 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20u?= =?UTF-8?q?seLayoutEffect=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/devices/DeviceSearchPage.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index f8f05c4..36c06b3 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -57,24 +57,14 @@ const DeviceSearchPage = () => { /* 모달 열림 상태 확인 및 스크롤 잠금 (회색 배경이 보일 때와 동일한 조건) */ const isModalOpen = (!!selectedProduct && !combo.showSaveCompleteModal) || combo.showSaveCompleteModal; - /* selectedProductId가 null이 될 때 명시적으로 스크롤 unlock */ - useLayoutEffect(() => { - if (selectedProductId === null) { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - } - }, [selectedProductId]); - - /* 모달 열림 상태에 따른 스크롤 lock */ + /* 모달 열림 상태에 따른 스크롤 lock/unlock */ useLayoutEffect(() => { if (isModalOpen) { document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; - - return () => { - document.documentElement.style.overflow = ''; - document.body.style.overflow = ''; - }; + } else { + document.documentElement.style.overflow = ''; + document.body.style.overflow = ''; } }, [isModalOpen]); From 2a880594fa1f51a165153a9e60cf50772a6c4056 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 22:52:22 +0900 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20isModalOpen=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=AA=85=ED=99=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/devices/DeviceSearchPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 36c06b3..4f0eb82 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -54,8 +54,9 @@ const DeviceSearchPage = () => { }, }); - /* 모달 열림 상태 확인 및 스크롤 잠금 (회색 배경이 보일 때와 동일한 조건) */ - const isModalOpen = (!!selectedProduct && !combo.showSaveCompleteModal) || combo.showSaveCompleteModal; + /* 모달 열림 상태 확인 및 스크롤 잠금 + * 기기 상세 모달 또는 저장 완료 모달이 열려있을 때 스크롤 잠금 */ + const isModalOpen = !!selectedProduct || combo.showSaveCompleteModal; /* 모달 열림 상태에 따른 스크롤 lock/unlock */ useLayoutEffect(() => { From 80a352bcf4172d14df1db8f5fce3fc8d1301d887 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 22:53:39 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20useCallback=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=BD=9C=EB=B0=B1=20=ED=95=A8=EC=88=98=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useScrollState.ts | 46 +++++++++++++------------- src/pages/devices/DeviceSearchPage.tsx | 22 ++++++++---- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/hooks/useScrollState.ts b/src/hooks/useScrollState.ts index fa4ce50..eebde81 100644 --- a/src/hooks/useScrollState.ts +++ b/src/hooks/useScrollState.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, type RefObject } from 'react'; +import { useState, useEffect, useCallback, type RefObject } from 'react'; import { SCROLL_CONSTANTS } from '@/constants/devices'; export const useScrollState = (productGridRef: RefObject) => { @@ -10,35 +10,35 @@ export const useScrollState = (productGridRef: RefObject) window.scrollTo(0, 0); }, []); - useEffect(() => { - const handleScroll = () => { - const scrollTop = window.scrollY; - const windowHeight = window.innerHeight; - const documentHeight = document.documentElement.scrollHeight; - - /* 맨 마지막 스크롤 도달 여부 체크 */ - const reachedBottom = - scrollTop + windowHeight >= documentHeight - SCROLL_CONSTANTS.BOTTOM_BUFFER; - setIsAtBottom(reachedBottom); - - /* 3행이 완전히 보일 때 Top 버튼 표시 */ - if (productGridRef.current) { - const gridTop = productGridRef.current.offsetTop; - const thirdRowVisible = - scrollTop + windowHeight >= gridTop + SCROLL_CONSTANTS.TOP_BUTTON_THRESHOLD; - setShowTopButton(thirdRowVisible); - } - }; + const handleScroll = useCallback(() => { + const scrollTop = window.scrollY; + const windowHeight = window.innerHeight; + const documentHeight = document.documentElement.scrollHeight; + + /* 맨 마지막 스크롤 도달 여부 체크 */ + const reachedBottom = + scrollTop + windowHeight >= documentHeight - SCROLL_CONSTANTS.BOTTOM_BUFFER; + setIsAtBottom(reachedBottom); + + /* 3행이 완전히 보일 때 Top 버튼 표시 */ + if (productGridRef.current) { + const gridTop = productGridRef.current.offsetTop; + const thirdRowVisible = + scrollTop + windowHeight >= gridTop + SCROLL_CONSTANTS.TOP_BUTTON_THRESHOLD; + setShowTopButton(thirdRowVisible); + } + }, [productGridRef]); + useEffect(() => { window.addEventListener('scroll', handleScroll); handleScroll(); return () => window.removeEventListener('scroll', handleScroll); - }, []); + }, [handleScroll]); - const handleScrollToTop = () => { + const handleScrollToTop = useCallback(() => { window.scrollTo({ top: 0, behavior: 'smooth' }); - }; + }, []); return { isAtBottom, diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 4f0eb82..11b9e7b 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useLayoutEffect } from 'react'; +import { useRef, useLayoutEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; import GNB from '@/components/Home/GNB'; import ProductCard from '@/components/ProductCard/ProductCard'; @@ -33,6 +33,19 @@ const DeviceSearchPage = () => { const productGridRef = useRef(null); const scroll = useScrollState(productGridRef); + /* 카테고리 클릭 핸들러 */ + const handleCategoryClick = useCallback((categoryId: number) => { + search.setSelectedCategory( + search.selectedCategory === categoryId ? null : categoryId + ); + }, [search.selectedCategory, search.setSelectedCategory]); + + /* 제품 클릭 핸들러 */ + const handleProductClick = useCallback((deviceId: number) => { + searchParams.set('productId', deviceId.toString()); + setSearchParams(searchParams); + }, [searchParams, setSearchParams]); + /* 선택된 제품 찾기 */ const selectedDevice = selectedProductId ? search.allDevices.find(d => d.deviceId === Number(selectedProductId)) @@ -96,7 +109,7 @@ const DeviceSearchPage = () => { return ( From 6c13540da19c897347a464ad653a0507a374b6c3 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 22:56:37 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20Input?= =?UTF-8?q?=EC=97=90=20aria-label=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/devices/DeviceSearchPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 4573d82..2c334e5 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -102,6 +102,7 @@ const DeviceSearchPage = () => { search.setSearchQuery(e.target.value)} className="flex-1 bg-transparent font-body-1-r text-gray-500 outline-none placeholder:text-gray-500" From 70883aed6f57f8a92ab172b47936a3ad9967e2b1 Mon Sep 17 00:00:00 2001 From: hun Date: Thu, 12 Feb 2026 23:00:04 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=8B=AC=EC=97=90=20E?= =?UTF-8?q?SC=20=ED=82=A4=EB=A1=9C=20=EB=8B=AB=EA=B8=B0=20=EB=B0=8F=20aria?= =?UTF-8?q?=20=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeviceSearch/DeviceDetailModal.tsx | 2 +- src/pages/devices/DeviceSearchPage.tsx | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/DeviceSearch/DeviceDetailModal.tsx b/src/components/DeviceSearch/DeviceDetailModal.tsx index 2f1275c..f7fc1d2 100644 --- a/src/components/DeviceSearch/DeviceDetailModal.tsx +++ b/src/components/DeviceSearch/DeviceDetailModal.tsx @@ -52,7 +52,7 @@ const DeviceDetailModal = ({
{/* Name & Price */}
-

{product.name}

+

{product.name}

{(product.price ?? 0).toLocaleString()}

diff --git a/src/pages/devices/DeviceSearchPage.tsx b/src/pages/devices/DeviceSearchPage.tsx index 2c334e5..5a2c990 100644 --- a/src/pages/devices/DeviceSearchPage.tsx +++ b/src/pages/devices/DeviceSearchPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useLayoutEffect, useCallback, useMemo } from 'react'; +import { useRef, useLayoutEffect, useCallback, useMemo, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import GNB from '@/components/Home/GNB'; import ProductCard from '@/components/ProductCard/ProductCard'; @@ -91,6 +91,22 @@ const DeviceSearchPage = () => { } }, [isModalOpen]); + /* ESC 키로 모달 닫기 */ + useEffect(() => { + const handleEscapeKey = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isModalOpen) { + combo.handleCloseModal(); + } + }; + + if (isModalOpen) { + document.addEventListener('keydown', handleEscapeKey); + return () => { + document.removeEventListener('keydown', handleEscapeKey); + }; + } + }, [isModalOpen, combo]); + return (
@@ -248,10 +264,22 @@ const DeviceSearchPage = () => {