diff --git a/client/src/components/CafeCard.tsx b/client/src/components/CafeCard.tsx index da3e9faa..69f5a1ba 100644 --- a/client/src/components/CafeCard.tsx +++ b/client/src/components/CafeCard.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react'; +import type { UIEventHandler } from 'react'; +import { useCallback, useState } from 'react'; import { styled } from 'styled-components'; import type { Cafe } from '../types'; import Resource from '../utils/Resource'; @@ -16,21 +17,12 @@ const CafeCard = (props: CardProps) => { const [isShowDetail, setIsShowDetail] = useState(false); const [currentImageIndex, setCurrentImageIndex] = useState(0); - const ref = useRef(null); - - useEffect(() => { - const handleScroll = () => { - if (ref.current) { - const { scrollLeft, clientWidth } = ref.current; - const index = Math.round(scrollLeft / clientWidth); - setCurrentImageIndex(index); - } - }; - - ref.current?.addEventListener('scroll', handleScroll); - return () => { - ref.current?.removeEventListener('scroll', handleScroll); - }; + const handleScroll: UIEventHandler = useCallback((event) => { + if (!(event.target instanceof HTMLDivElement)) return; + + const { scrollLeft, clientWidth } = event.target; + const index = Math.round(scrollLeft / clientWidth); + setCurrentImageIndex(index); }, []); return ( @@ -40,12 +32,13 @@ const CafeCard = (props: CardProps) => { {`${currentImageIndex + 1}`}/{cafe.images.length} - + {cafe.images.map((image, index) => ( ))} diff --git a/client/src/components/ScrollSnapContainer.tsx b/client/src/components/ScrollSnapContainer.tsx index 71cad391..33144c4c 100644 --- a/client/src/components/ScrollSnapContainer.tsx +++ b/client/src/components/ScrollSnapContainer.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import type { HTMLAttributes, MouseEventHandler, TouchEventHandler, WheelEventHandler } from 'react'; +import type { HTMLAttributes, MouseEventHandler, PropsWithChildren, TouchEventHandler, WheelEventHandler } from 'react'; import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -66,14 +66,38 @@ const SWIPE_FAST_SCROLL_DISTANCE_RATIO = 0.03; // 0 = 다음 아이템으로 완전히 넘어가야 스와이프 판정 가능 const SWIPE_WHEEL_SCROLL_VALID_RATIO = 0.1; +type ScrollSnapVirtualItemProps = PropsWithChildren<{ + // 이전 아이템인지, 현재 아이템인지, 이후 아이템인지 여부를 나타내는 숫자 + offset: -1 | 0 | 1; + // 0.0 ~ 1.0 + position: number; +}>; + +const ScrollSnapVirtualItem = (props: ScrollSnapVirtualItemProps) => { + const { offset, position, children } = props; + + return ( +
+ {children} +
+ ); +}; + type ScrollSnapVirtualItemsProps = { scrollPosition: number; items: Item[]; itemRenderer: (item: Item, index: number) => React.ReactNode; + enableRolling?: boolean; }; const ScrollSnapVirtualItems = (props: ScrollSnapVirtualItemsProps) => { - const { scrollPosition, items, itemRenderer } = props; + const { scrollPosition, items, itemRenderer, enableRolling } = props; // position of item, which user sees // always positive integer @@ -83,11 +107,17 @@ const ScrollSnapVirtualItems = (props: ScrollSnapVirtualItemsProps) const indexedItems = items.map((item, index) => ({ index, item })); // 현재 화면의 아이템 및 위, 아래의 아이템을 표시한다 - const visibleItems = [ - indexedItems[focusedIndex - 1] ?? indexedItems[indexedItems.length - 1], - indexedItems[focusedIndex], - indexedItems[focusedIndex + 1] ?? indexedItems[0], - ]; + const visibleItems = enableRolling + ? [ + indexedItems.at(focusedIndex - 1), // 현재에서 상단 (혹은 끝) 아이템 + indexedItems.at(focusedIndex), // 현재 아이템 + indexedItems.at((focusedIndex + 1) % indexedItems.length), // 현재에서 하단 (혹은 첫) 아이템 + ] + : [ + indexedItems[focusedIndex - 1], // 현재에서 상단 아이템 + indexedItems[focusedIndex], // 현재 아이템 + indexedItems[focusedIndex + 1], // 현재에서 하단 아이템 + ]; const visiblePosition = mod(scrollPosition, 1); return ( @@ -95,10 +125,20 @@ const ScrollSnapVirtualItems = (props: ScrollSnapVirtualItemsProps) style={{ width: '100%', height: '100%', - transform: `translateY(${-100 + -visiblePosition * 100}%)`, + display: 'grid', }} > - {visibleItems.map(({ item, index }) => itemRenderer(item, index))} + {([0, 1, -1] as const) + .map((visibleIndex) => ({ ...visibleItems[1 + visibleIndex], visibleIndex })) + .map(({ item, index, visibleIndex }) => + item && typeof index === 'number' ? ( + + {itemRenderer(item, index)} + + ) : ( + + ), + )} ); }; @@ -380,7 +420,7 @@ const ScrollSnapContainer = (props: ScrollSnapContainerProps) => { onMouseLeave={handleMouseLeave} onWheel={handleWheel} > - + ); };