diff --git a/src/@components/@common/hooks/useDraggableYContainer.ts b/src/@components/@common/hooks/useDraggableYContainer.ts deleted file mode 100644 index 4fd27299..00000000 --- a/src/@components/@common/hooks/useDraggableYContainer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useRef, useState } from "react"; - -export default function useDraggableYContainer() { - const containerRef = useRef(null); - - const [isStartDragging, setIsStartDragging] = useState(false); - const [isScrollEnd, setIsScrollEnd] = useState(false); - const [startY, setStartY] = useState(0); - const [movedY, setMovedY] = useState(0); - - const touchSensitivity = 0.7; // 이동 감도 조절 값 (조절할 수 있음) - - function handleTouchStart(event: React.TouchEvent) { - setIsStartDragging(true); - setStartY(event.touches[0].clientY); // 터치 시작 지점 저장 - setMovedY(0); - } - - function handleTouchMove(event: React.TouchEvent) { - if (!isStartDragging) return; - - const deltaY = event.touches[0].clientY - startY; - - // 이동 감도에 따라 이동 거리 조절 - const adjustedDeltaY = deltaY / touchSensitivity; - - setMovedY(adjustedDeltaY); - moveContainer(adjustedDeltaY); - } - - function moveContainer(deltaY: number) { - const container = containerRef.current; - if (!container) return; - - container.scrollTop -= deltaY; - - // 컨테이너 끝에 도달했는지 여부 확인 - setIsScrollEnd(container.scrollHeight - container.scrollTop === container.clientHeight); - } - - function handleTouchEnd() { - setIsStartDragging(false); - } - - return { - scrollableContainerProps: { - ref: containerRef, - onTouchStart: handleTouchStart, - onTouchMove: handleTouchMove, - onTouchEnd: handleTouchEnd, - }, - isDragging: Math.abs(movedY) > 10, // 이동 거리에 따라 드래그 여부 결정 - isScrollEnd, - }; -} diff --git a/src/@components/@common/hooks/useDraggingContainer.ts b/src/@components/@common/hooks/useDraggingContainer.ts index 42e107ea..91993f28 100644 --- a/src/@components/@common/hooks/useDraggingContainer.ts +++ b/src/@components/@common/hooks/useDraggingContainer.ts @@ -1,50 +1,118 @@ import { useRef, useState } from "react"; -export default function useDraggingContainer() { +type DragDirectionType = "X" | "Y"; + +type EventMapperType = { + [key in DragDirectionType]: "pageX" | "pageY"; +}; + +const eventMapper: EventMapperType = { + X: "pageX", + Y: "pageY", +}; + +const FIRST_TOUCH_EVENT_IDX = 0; + +export default function useDraggingContainer(dragDirection: DragDirectionType) { const containerRef = useRef(null); - const [isStartDragging, setIsStartDragging] = useState(false); - const currentX = useRef(0); + const currentRef = useRef(0); + const standardRef = useRef(0); - const standardX = useRef(0); - const [draggedX, setDraggedX] = useState(0); + const [isStartDragging, setIsStartDragging] = useState(false); + const [isArrivedEnd, setIsArrivedEnd] = useState(false); + const [draggedDistance, setDraggedDistance] = useState(0); function handleMouseDown(event: React.MouseEvent) { setIsStartDragging(true); - currentX.current = event.pageX; + const page = getPageByEventType(event); + currentRef.current = page; + initializeForDraggedDistance(page); + } - initializeForDraggedX(event.pageX); + function handleTouchStart(event: React.TouchEvent) { + setIsStartDragging(true); + + const page = getPageByEventType(event); + currentRef.current = page; + initializeForDraggedDistance(page); + } + + function getPageByEventType(event: React.SyntheticEvent): number { + const eventType = eventMapper[dragDirection]; + if (event.nativeEvent instanceof TouchEvent) { + return event.nativeEvent.touches[FIRST_TOUCH_EVENT_IDX][eventType]; + } + if (event.nativeEvent instanceof MouseEvent) { + return event.nativeEvent[eventType]; + } + return 0; } - function initializeForDraggedX(standartX: number) { - setDraggedX(0); - standardX.current = standartX; + function initializeForDraggedDistance(standard: number) { + setDraggedDistance(0); + standardRef.current = standard; } function handleMouseMove(event: React.MouseEvent) { const container = containerRef.current; + + if (!container) return; + if (!isStartDragging) return; + + const page = getPageByEventType(event); + moveContainerByCurrent(container, page); + handleArrivedEnd(container); + + setDraggedDistance(Math.abs(page - standardRef.current)); + } + + function handleTouchMove(event: React.TouchEvent) { + const container = containerRef.current; + if (!container) return; if (!isStartDragging) return; - moveContainerByCurrentX(container, event.pageX); + const page = getPageByEventType(event); + moveContainerByCurrent(container, page); + handleArrivedEnd(container); - setDraggedX(Math.abs(event.pageX - standardX.current)); + setDraggedDistance(Math.abs(page - standardRef.current)); } - function moveContainerByCurrentX(container: HTMLElement, movedMouseX: number) { - container.scrollLeft += currentX.current - movedMouseX; - currentX.current = movedMouseX; + function moveContainerByCurrent(container: HTMLElement, movedTrigger: number) { + const delta = currentRef.current - movedTrigger; + if (dragDirection === "X") { + container.scrollLeft += delta; + } + if (dragDirection === "Y") { + container.scrollTop += delta; + } + currentRef.current = movedTrigger; + } + + function handleArrivedEnd(container: HTMLElement) { + if (dragDirection === "X") { + setIsArrivedEnd(container.scrollWidth - container.scrollLeft === container.clientWidth); + } + if (dragDirection === "Y") { + setIsArrivedEnd(container.scrollHeight - container.scrollTop === container.clientHeight); + } } function handleMouseUpOrLeave() { reset(); } + function handleTouchEndOrCancel() { + reset(); + } + function reset() { setIsStartDragging(false); - currentX.current = 0; - standardX.current = 0; + currentRef.current = 0; + standardRef.current = 0; } return { @@ -54,7 +122,13 @@ export default function useDraggingContainer() { onMouseMove: handleMouseMove, onMouseUp: handleMouseUpOrLeave, onMouseLeave: handleMouseUpOrLeave, + + onTouchStart: handleTouchStart, + onTouchMove: handleTouchMove, + onTouchEnd: handleTouchEndOrCancel, + onTouchCancel: handleTouchEndOrCancel, }, - isDragging: draggedX > 10, + isDragging: draggedDistance > 10, + isArrivedEnd, }; } diff --git a/src/@components/@common/hooks/useDrawer.ts b/src/@components/@common/hooks/useDrawer.ts index e5d7af9a..49c42ca7 100644 --- a/src/@components/@common/hooks/useDrawer.ts +++ b/src/@components/@common/hooks/useDrawer.ts @@ -1,95 +1,149 @@ -import { useCallback, useRef, useState } from "react"; +import { useRef, useState } from "react"; const thresholds = { - offset: 150, // 이 이상 내려야 drawer 닫힘 - transitionTime: 0.2, // 애니메이션 지속 시간 + OFFSET: 150, + ANIMATION_TRANSITION_TIME: 0.2, }; +const FIRST_TOUCH_EVENT_IDX = 0; + export default function useDrawer(closeModal: () => void) { const knobRef = useRef(null); const containerRef = useRef(null); - const [isDragging, setIsDragging] = useState(false); - const [dragStart, setDragStart] = useState({ y: 0 }); - const [yOffset, setYOffset] = useState(0); + const currentRef = useRef(0); + const standardRef = useRef(0); + + const [isStartDragging, setIsStartDragging] = useState(false); + const [draggedDistance, setDraggedDistance] = useState(0); + + function handleMouseDown(event: React.MouseEvent) { + const knob = knobRef.current; + if (!knob) return; + if (isNode(event.target) && !knob.contains(event.target)) return; + setIsStartDragging(true); + + const page = getPageByEventType(event); + currentRef.current = page; + initializeForDraggedDistance(page); + } + + function handleTouchStart(event: React.TouchEvent) { + const knob = knobRef.current; + if (!knob) return; + if (isNode(event.target) && !knob.contains(event.target)) return; + setIsStartDragging(true); - const handleMouseDown = useCallback((e: React.MouseEvent) => { - if (knobRef.current && knobRef.current.contains(e.target as Node)) { - setIsDragging(true); - setDragStart({ - y: e.clientY, - }); + const page = getPageByEventType(event); + currentRef.current = page; + initializeForDraggedDistance(page); + } + + function isNode(target: EventTarget): target is Node { + return (target as Node) !== undefined; + } + + function getPageByEventType(event: React.SyntheticEvent): number { + if (event.nativeEvent instanceof TouchEvent) { + return event.nativeEvent.touches[FIRST_TOUCH_EVENT_IDX].pageY; } - }, []); + if (event.nativeEvent instanceof MouseEvent) { + return event.nativeEvent.pageY; + } + return 0; + } - const handleMouseMove = useCallback( - (e: React.MouseEvent) => { - if (!isDragging) return; + function initializeForDraggedDistance(standard: number) { + setDraggedDistance(0); + standardRef.current = standard; + } - const offset = Math.max(0, e.clientY - dragStart.y); + function handleMouseMove(event: React.MouseEvent) { + const container = containerRef.current; - // 모달을 드래그한 만큼 이동 - const container = containerRef.current; - if (!container) return; - container.style.transform = `translateY(${offset}px)`; - setYOffset(offset); - }, - [dragStart.y, isDragging], - ); + if (!container) return; + if (!isStartDragging) return; + + const page = getPageByEventType(event); + currentRef.current = page; + moveDrawerByCurrent(container, page); + + setDraggedDistance(Math.abs(page - standardRef.current)); + } - const handleMouseUp = useCallback(() => { + function handleTouchMove(event: React.TouchEvent) { const container = containerRef.current; + if (!container) return; + if (!isStartDragging) return; + + const page = getPageByEventType(event); + currentRef.current = page; + moveDrawerByCurrent(container, page); + + setDraggedDistance(Math.abs(page - standardRef.current)); + } + + function moveDrawerByCurrent(container: HTMLElement, movedTrigger: number) { + const offset = Math.max(0, movedTrigger - standardRef.current); + container.style.transform = `translateY(${offset}px)`; + currentRef.current = movedTrigger; + } - if (yOffset > 150) { - container.style.transition = `transform ${thresholds.transitionTime}s ease-in-out`; + function handleMouseUpOrLeave() { + const container = containerRef.current; + if (!container) return; + + if (draggedDistance > thresholds.OFFSET) { + container.style.transition = `transform ${thresholds.ANIMATION_TRANSITION_TIME}s ease-in-out`; container.style.transform = "translateY(100%)"; setTimeout(() => { closeModal(); - }, thresholds.transitionTime * 1000 + 50); + }, thresholds.ANIMATION_TRANSITION_TIME * 1000 + 50); } else { - container.style.transition = `transform ${thresholds.transitionTime / 2}s ease-out`; + container.style.transition = `transform ${thresholds.ANIMATION_TRANSITION_TIME / 2}s ease-out`; container.style.transform = "translateY(0)"; } - setIsDragging(false); - }, [yOffset, closeModal]); - - /** - * For Mobile - * */ - - const handleTouchStart = useCallback((e: React.TouchEvent) => { - if (knobRef.current && knobRef.current.contains(e.target as Node)) { - setIsDragging(true); - setDragStart({ - y: e.touches[0].clientY, - }); - } - }, []); - const handleTouchMove = useCallback( - (e: React.TouchEvent) => { - if (!isDragging) return; + reset(); + } - const offset = Math.max(0, e.touches[0].clientY - dragStart.y); + function handleTouchEndOrCancel() { + const container = containerRef.current; + if (!container) return; - const container = containerRef.current; - if (!container) return; - container.style.transform = `translateY(${offset}px)`; - setYOffset(offset); - }, - [dragStart.y, isDragging], - ); + if (draggedDistance > thresholds.OFFSET) { + container.style.transition = `transform ${thresholds.ANIMATION_TRANSITION_TIME}s ease-in-out`; + container.style.transform = "translateY(100%)"; + setTimeout(() => { + closeModal(); + }, thresholds.ANIMATION_TRANSITION_TIME * 1000 + 50); + } else { + container.style.transition = `transform ${thresholds.ANIMATION_TRANSITION_TIME / 2}s ease-out`; + container.style.transform = "translateY(0)"; + } + + reset(); + } + + function reset() { + setIsStartDragging(false); + currentRef.current = 0; + standardRef.current = 0; + } return { drawerProps: { ref: containerRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, - onMouseUp: handleMouseUp, + onMouseUp: handleMouseUpOrLeave, + onMouseLeave: handleMouseUpOrLeave, + onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, - onTouchEnd: handleMouseUp, + onTouchEnd: handleTouchEndOrCancel, + onTouchCancel: handleTouchEndOrCancel, }, knobRef, }; diff --git a/src/@components/@common/hooks/useShowByQuery.ts b/src/@components/@common/hooks/useShowByQuery.ts deleted file mode 100644 index 4d2240a7..00000000 --- a/src/@components/@common/hooks/useShowByQuery.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; - -import { LocationType } from "../../../types/cardCollection"; - -// 파라미터로 보이지 않아야할 locationType 전달 -export default function useShowByCardType(locationTypes: LocationType[]) { - const [isShow, setIsShow] = useState(false); - const [searchParams] = useSearchParams(); - - useEffect(() => { - const cardType = searchParams.get("type"); - setIsShow(!locationTypes.includes((cardType as LocationType) || "")); - }, [locationTypes]); - - return { isShow }; -} diff --git a/src/@components/BestPiicklePage/BestPiickleRecommend/RecommendItem/index.tsx b/src/@components/BestPiicklePage/BestPiickleRecommend/RecommendItem/index.tsx index 2faa4972..0789a859 100644 --- a/src/@components/BestPiicklePage/BestPiickleRecommend/RecommendItem/index.tsx +++ b/src/@components/BestPiicklePage/BestPiickleRecommend/RecommendItem/index.tsx @@ -11,7 +11,7 @@ const BEST_PIICKLE_TOTAL_COUNT = 4; export default function RecommendItem(props: RecommendProps) { const { recommendList } = props; - const { scrollableContainerProps, isDragging } = useDraggingContainer(); + const { scrollableContainerProps, isDragging } = useDraggingContainer("X"); return ( diff --git a/src/@components/BestPiicklePage/BestPiickleRecommend/index.tsx b/src/@components/BestPiicklePage/BestPiickleRecommend/index.tsx index 165429ee..74131313 100644 --- a/src/@components/BestPiicklePage/BestPiickleRecommend/index.tsx +++ b/src/@components/BestPiicklePage/BestPiickleRecommend/index.tsx @@ -1,8 +1,8 @@ import { CardList, LocationType } from "../../../types/cardCollection"; import { HeadingTitle } from "../../../util/main/headingTitles"; import HeadingTitleContainer from "../../@common/HeadingTitleContainer"; -import { useRecentlyBookmarked } from "../../@common/hooks/useRecentlyBookmarked"; -import { useCardsByGender } from "./hooks/useCardsByGender"; +import { useCardsByGender } from "../hooks/useCardsByGender"; +import { useRecentlyBookmarked } from "../hooks/useRecentlyBookmarked"; import RecommendItem from "./RecommendItem"; import * as St from "./style"; diff --git a/src/@components/BestPiicklePage/BestPiickleRecommend/hooks/useCardsByGender.ts b/src/@components/BestPiicklePage/hooks/useCardsByGender.ts similarity index 56% rename from src/@components/BestPiicklePage/BestPiickleRecommend/hooks/useCardsByGender.ts rename to src/@components/BestPiicklePage/hooks/useCardsByGender.ts index 3d11add8..4656a7d9 100644 --- a/src/@components/BestPiicklePage/BestPiickleRecommend/hooks/useCardsByGender.ts +++ b/src/@components/BestPiicklePage/hooks/useCardsByGender.ts @@ -1,9 +1,9 @@ import useSWR from "swr"; -import { realReq } from "../../../../core/api/common/axios"; -import { PATH } from "../../../../core/api/common/constants"; -import { CardList } from "../../../../types/cardCollection"; -import { PiickleSWRResponse } from "../../../../types/remote/swr"; +import { realReq } from "../../../core/api/common/axios"; +import { PATH } from "../../../core/api/common/constants"; +import { CardList } from "../../../types/cardCollection"; +import { PiickleSWRResponse } from "../../../types/remote/swr"; export function useCardsByGender(gender: "남" | "여") { const { data } = useSWR>( diff --git a/src/@components/@common/hooks/useRecentlyBookmarked.ts b/src/@components/BestPiicklePage/hooks/useRecentlyBookmarked.ts similarity index 100% rename from src/@components/@common/hooks/useRecentlyBookmarked.ts rename to src/@components/BestPiicklePage/hooks/useRecentlyBookmarked.ts diff --git a/src/@components/CardCollectionPage/Card/CommentModal/index.tsx b/src/@components/CardCollectionPage/Card/CommentModal/index.tsx index 17f447c4..873d3762 100644 --- a/src/@components/CardCollectionPage/Card/CommentModal/index.tsx +++ b/src/@components/CardCollectionPage/Card/CommentModal/index.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import IcSubmitBtn from "../../../../asset/icon/IcSubmitBtn"; -import useDraggableYContainer from "../../../@common/hooks/useDraggableYContainer"; +import useDraggingContainer from "../../../@common/hooks/useDraggingContainer"; import useDrawer from "../../../@common/hooks/useDrawer"; import Modal from "../../../@common/Modal"; import { CommentList, handleCommentController } from "../../hooks/useComments"; @@ -16,7 +16,7 @@ interface CommentModalProps { export default function CommentModal(props: CommentModalProps) { const { cardId, comments, onClickBackground, handleSubmitComment } = props; - const { scrollableContainerProps, isScrollEnd } = useDraggableYContainer(); + const { scrollableContainerProps, isArrivedEnd } = useDraggingContainer("Y"); const { drawerProps, knobRef } = useDrawer(onClickBackground); const [answer, setAnswer] = useState(""); @@ -46,7 +46,7 @@ export default function CommentModal(props: CommentModalProps) { ))} - {!isScrollEnd && } + {!isArrivedEnd && } ) { const navigate = useNavigate(); const { showToast } = useToast(); + const { cardType } = useCardType(); + const [isReplayBtnVisible, setIsReplayBtnVisible] = useState(false); const showToastFlag = !!sessionStorage.getItem(TOAST_SESSON_KEY); - const { isShow: isReplayBtnShow } = useShowByCardType([ - LocationType.BEST, - LocationType.MEDLEY, - LocationType.RECENT, - LocationType.BOOKMARK, - LocationType.MALE, - LocationType.FEMALE, - ]); useEffect(() => { if (showToastFlag) { @@ -31,6 +33,10 @@ const LastCard = forwardRef(function LastCard(_, ref: React.ForwardedRef { + cardType && setIsReplayBtnVisible(!noReplayLocationTypes.includes(cardType)); + }, [cardType]); + const reloadForSimilarTopic = () => { sessionStorage.setItem(TOAST_SESSON_KEY, "true"); window.location.reload(); @@ -44,7 +50,7 @@ const LastCard = forwardRef(function LastCard(_, ref: React.ForwardedRef끊임없는 대화를 위해 버튼을 선택해주세요 - {isReplayBtnShow && ( + {isReplayBtnVisible && ( 비슷한 주제 계속 보기 diff --git a/src/@components/CardCollectionPage/Card/MenuModal/index.tsx b/src/@components/CardCollectionPage/Card/MenuModal/index.tsx index 776c1def..f9175d2a 100644 --- a/src/@components/CardCollectionPage/Card/MenuModal/index.tsx +++ b/src/@components/CardCollectionPage/Card/MenuModal/index.tsx @@ -1,6 +1,8 @@ +import { useEffect, useState } from "react"; + import { LocationType } from "../../../../types/cardCollection"; import { GTM_CLASS_NAME } from "../../../../util/const/gtm"; -import useShowByCardType from "../../../@common/hooks/useShowByQuery"; +import useCardType from "../../../@common/hooks/useCardType"; import Modal from "../../../@common/Modal"; import useToast from "../../../@common/Toast/hooks/useToast"; import { handleClickBlacklistType } from "../../hooks/useBlacklist"; @@ -23,11 +25,18 @@ type ModalItem = { gtmClassName: string; }; +const noBlockLocationTypes = [LocationType.BEST, LocationType.MEDLEY]; + export default function MenuModal(props: MenuModalProps) { const { currentCardId, closeHandler, autoSlide, handleClickAddBlacklist, handleClickCancelBlacklist } = props; const { showToast, blackoutToast } = useToast(); - const { isShow: isBlockShow } = useShowByCardType([LocationType.BEST, LocationType.MEDLEY]); + const { cardType } = useCardType(); + const [isBlockVisible, setIsBlockVisible] = useState(false); + + useEffect(() => { + cardType && setIsBlockVisible(!noBlockLocationTypes.includes(cardType)); + }, [cardType]); const onSuccessAddBlacklist = () => { closeHandler(); @@ -79,7 +88,7 @@ export default function MenuModal(props: MenuModalProps) { {ModalItems.map(({ emoji, title, isNeedLogin, handleClickItem, gtmClassName }, idx) => { - if (idx === 1 && !isBlockShow) { + if (idx === 1 && !isBlockVisible) { return null; } else { return ( diff --git a/src/@components/CardCollectionPage/style.ts b/src/@components/CardCollectionPage/style.ts index d2ba81e6..72ee7f63 100644 --- a/src/@components/CardCollectionPage/style.ts +++ b/src/@components/CardCollectionPage/style.ts @@ -20,6 +20,7 @@ export const EventCoach = styled.div` background: linear-gradient(0deg, #fff 0%, rgba(255, 255, 255, 0) 100%); + pointer-events: none; z-index: 10; `; diff --git a/src/@components/MainPage/Banner/index.tsx b/src/@components/MainPage/Banner/index.tsx index c58cfa88..695365f7 100644 --- a/src/@components/MainPage/Banner/index.tsx +++ b/src/@components/MainPage/Banner/index.tsx @@ -8,7 +8,7 @@ import { LocationType } from "../../../types/cardCollection"; import { externalLinks } from "../../../util/const/externalLinks"; import { GTM_CLASS_NAME } from "../../../util/const/gtm"; import { newBannerImages, newBannerType } from "../../../util/main/banner"; -import { useRecentlyBookmarked } from "../../@common/hooks/useRecentlyBookmarked"; +import { useRecentlyBookmarked } from "../../BestPiicklePage/hooks/useRecentlyBookmarked"; import useBannerSwiper from "../hooks/useBannerSwiper"; import { useRecentlyUpdated } from "../hooks/useRecentlyUpdated"; import Slide from "./Slide"; diff --git a/src/@components/MainPage/BestPiickle/index.tsx b/src/@components/MainPage/BestPiickle/index.tsx index 62bd8838..e08daf89 100644 --- a/src/@components/MainPage/BestPiickle/index.tsx +++ b/src/@components/MainPage/BestPiickle/index.tsx @@ -10,7 +10,7 @@ import St from "./style"; export default function BestPiickle() { const { bestPiickle } = useBestPiickle(); - const { scrollableContainerProps, isDragging } = useDraggingContainer(); + const { scrollableContainerProps, isDragging } = useDraggingContainer("X"); return ( diff --git a/src/@components/MainPage/Medley/index.tsx b/src/@components/MainPage/Medley/index.tsx index 74c2a4fd..c7e4e593 100644 --- a/src/@components/MainPage/Medley/index.tsx +++ b/src/@components/MainPage/Medley/index.tsx @@ -4,7 +4,7 @@ import MedleyCard from "./MedleyCard"; import * as St from "./style"; export default function Medley() { - const { scrollableContainerProps, isDragging } = useDraggingContainer(); + const { scrollableContainerProps, isDragging } = useDraggingContainer("X"); const { randomMedleyLists } = useMedleyLists(); return (