From 96e54bdbae7cea6acc3c987fd5346b52311012ea Mon Sep 17 00:00:00 2001 From: mokjakA Date: Wed, 12 Aug 2026 14:59:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(S15P11A705-441):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=B9=B4=EB=93=9C=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/SearchResultGallery.test.tsx | 38 ++++++++++++++++++- .../home/components/SearchResultGallery.tsx | 7 +++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/features/home/components/SearchResultGallery.test.tsx b/src/features/home/components/SearchResultGallery.test.tsx index 5253104..ee467f4 100644 --- a/src/features/home/components/SearchResultGallery.test.tsx +++ b/src/features/home/components/SearchResultGallery.test.tsx @@ -119,11 +119,12 @@ describe('SearchResultGallery', () => { const pages = Array.from(container.querySelectorAll('.pl-results__page')); const firstCard = container.querySelector('.pl-results__card')!; const scrollTo = vi.fn(); + const setPointerCapture = vi.fn(); Object.defineProperties(gallery, { scrollLeft: { configurable: true, writable: true, value: 0 }, scrollTo: { configurable: true, value: scrollTo }, - setPointerCapture: { configurable: true, value: vi.fn() }, + setPointerCapture: { configurable: true, value: setPointerCapture }, hasPointerCapture: { configurable: true, value: () => true }, releasePointerCapture: { configurable: true, value: vi.fn() }, }); @@ -149,10 +150,45 @@ describe('SearchResultGallery', () => { }); expect(gallery.scrollLeft).toBe(600); + expect(setPointerCapture).toHaveBeenCalledWith(1); expect(scrollTo).toHaveBeenCalledWith({ left: 960, behavior: 'smooth' }); expect(onSelectRecord).not.toHaveBeenCalled(); act(() => firstCard.click()); expect(onSelectRecord).toHaveBeenCalledWith(1); }); + + it('카드의 pointerdown과 pointerup 사이에 드래그가 없으면 capture하지 않고 상세를 연다', () => { + const onSelectRecord = vi.fn(); + act(() => { + root.render(); + }); + + const gallery = container.querySelector('.pl-results__gallery')!; + const firstCard = container.querySelector('.pl-results__card')!; + const setPointerCapture = vi.fn(); + + Object.defineProperties(gallery, { + setPointerCapture: { configurable: true, value: setPointerCapture }, + hasPointerCapture: { configurable: true, value: () => false }, + }); + + act(() => { + firstCard.dispatchEvent( + Object.assign(new MouseEvent('pointerdown', { bubbles: true, clientX: 400, button: 0 }), { + pointerId: 2, + }), + ); + firstCard.dispatchEvent( + Object.assign(new MouseEvent('pointerup', { bubbles: true, clientX: 400, button: 0 }), { + pointerId: 2, + }), + ); + firstCard.click(); + }); + + expect(setPointerCapture).not.toHaveBeenCalled(); + expect(onSelectRecord).toHaveBeenCalledOnce(); + expect(onSelectRecord).toHaveBeenCalledWith(1); + }); }); diff --git a/src/features/home/components/SearchResultGallery.tsx b/src/features/home/components/SearchResultGallery.tsx index 997b13e..3985f22 100644 --- a/src/features/home/components/SearchResultGallery.tsx +++ b/src/features/home/components/SearchResultGallery.tsx @@ -108,7 +108,6 @@ export function SearchResultGallery({ items, onSelectRecord }: SearchResultGalle didDragRef.current = false; isDraggingRef.current = true; setIsDragging(true); - gallery.setPointerCapture(event.pointerId); }; const handlePointerMove = (event: React.PointerEvent) => { @@ -118,6 +117,12 @@ export function SearchResultGallery({ items, onSelectRecord }: SearchResultGalle } const deltaX = event.clientX - dragStartXRef.current; if (Math.abs(deltaX) >= DRAG_THRESHOLD_PX) { + if (!didDragRef.current) { + // pointerdown에서 바로 capture하면 브라우저가 pointerup/click의 대상을 카드가 아니라 + // 갤러리로 재지정한다. hover는 보이는데 카드 onClick만 사라졌던 실제 원인이다. + // 단순 클릭은 원래 button을 끝까지 대상으로 유지하고, 드래그가 확정된 뒤에만 capture한다. + gallery.setPointerCapture(event.pointerId); + } didDragRef.current = true; } if (didDragRef.current) {