This commit is contained in:
focp212@naver.com
2025-10-22 14:21:14 +09:00
parent 1e7f13d5cc
commit 556b3f2a6a
27 changed files with 178 additions and 397 deletions

View File

@@ -8,7 +8,7 @@ type CommonErrorProps = FallbackProps & {
height?: number;
};
export const APIError = ({ error, resetErrorBoundary }: CommonErrorProps) => {
const { navigateBack } = useNavigate();
const { reload } = useNavigate();
const msg = useMemo(() => {
let message: Partial<DialogProps> = {
title: '일시적인 오류가 발생하였습니다.',
@@ -21,7 +21,7 @@ export const APIError = ({ error, resetErrorBoundary }: CommonErrorProps) => {
}, [error]);
const handleCancel = () => {
navigateBack();
reload();
resetErrorBoundary();
};
@@ -30,8 +30,8 @@ export const APIError = ({ error, resetErrorBoundary }: CommonErrorProps) => {
afterLeave={() => null}
open={true}
onClose={() => null}
onConfirmClick={resetErrorBoundary}
onCancelClick={handleCancel}
onConfirmClick={ resetErrorBoundary }
onCancelClick={ handleCancel }
message={msg.message}
buttonLabel={['취소', '재시도']}
/>

View File

@@ -1,15 +0,0 @@
import { Fragment } from 'react/jsx-runtime';
import { useAppColor } from '@/shared/lib/hooks/use-change-bg-color';
export const IOSStatusBar = () => {
const [appColor] = useAppColor();
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if(!isIOS){
return <Fragment></Fragment>;
}
return (
<div className="ios-status-bar" style={{ backgroundColor: appColor }}></div>
);
};

View File

@@ -1,21 +0,0 @@
interface EventListeners {
element: HTMLElement | null;
onScroll: () => void;
onTouchStart: () => void;
onTouchMove: () => void;
onTouchEnd: () => void;
}
export const addEventListeners = ({ element, onScroll, onTouchStart, onTouchMove, onTouchEnd }: EventListeners) => {
element?.addEventListener('scroll', onScroll);
element?.addEventListener('touchstart', onTouchStart, { passive: true });
element?.addEventListener('touchmove', onTouchMove, { passive: true });
element?.addEventListener('touchend', onTouchEnd, { passive: true });
};
export const removeEventListeners = ({ element, onScroll, onTouchStart, onTouchMove, onTouchEnd }: EventListeners) => {
element?.removeEventListener('scroll', onScroll);
element?.removeEventListener('touchstart', onTouchStart);
element?.removeEventListener('touchmove', onTouchMove);
element?.removeEventListener('touchend', onTouchEnd);
};

View File

@@ -1,103 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { AnimatePresence, motion, useMotionValue } from 'framer-motion';
import { useCallback, useRef, useState } from 'react';
import { useRouteChangeEffect } from './use-route-change-effect';
import { useScrollEventListeners } from './use-scroll-event-listeners';
import { useScrollTarget } from './use-scroll-target';
const TIME_OUT = 5000;
const THRASH_HOLD = 30;
let isScrollingBackToTop = false;
let lastScrollY = 0;
let isTouching = false;
export const TopButton = () => {
const scrollTarget = useScrollTarget();
const scrollY = useMotionValue(scrollTarget.current?.scrollTop ?? 0);
const scrollTimeOutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const [showButton, setShowButton] = useState<boolean | undefined>();
const reset = useCallback(() => {
setShowButton(false);
isScrollingBackToTop = false;
isTouching = false;
if (scrollTarget.current) {
scrollTarget.current.scrollTop = 0;
}
clearTimeout(scrollTimeOutRef.current);
}, []);
const initialize = useCallback(() => {
reset();
lastScrollY = 0;
}, []);
const scrollBackToTop = useCallback(() => {
reset();
}, []);
const onScroll = () => {
if (isTouching) return;
const currentScrollY = scrollTarget.current?.scrollTop ?? 0;
const direction = currentScrollY > lastScrollY ? 'down' : 'up';
lastScrollY = currentScrollY <= 0 ? 0 : currentScrollY;
scrollY.set(currentScrollY);
clearTimeout(scrollTimeOutRef.current);
setShowButton(direction === 'down' ? false : currentScrollY > THRASH_HOLD);
if (!isScrollingBackToTop && direction === 'down') {
scrollTimeOutRef.current = setTimeout(() => {
setShowButton(true);
}, TIME_OUT);
}
if (currentScrollY === 0) {
isScrollingBackToTop = false;
}
};
const onTouchStart = useCallback(() => {
isTouching = true;
}, []);
const onTouchMove = useCallback(() => {
isTouching = true;
}, []);
const onTouchEnd = useCallback(() => {
isTouching = false;
}, []);
useScrollEventListeners({
scrollTarget,
onScroll,
onTouchStart,
onTouchMove,
onTouchEnd,
});
useRouteChangeEffect({
scrollTarget,
onScroll,
onTouchStart,
onTouchMove,
onTouchEnd,
callback: initialize,
});
return (
<AnimatePresence>
{showButton && (
<motion.button
onClick={scrollBackToTop}
className={'btn goto-top-btn'}
initial={{ translateY: '110%' }}
animate={{ translateY: '0%' }}
>
</motion.button>
)}
</AnimatePresence>
);
};

View File

@@ -1,53 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useIsFetching, useIsMutating } from '@tanstack/react-query';
import { useCallback, useEffect } from 'react';
import { useRouterListener } from '@/shared/lib/hooks';
import { addEventListeners, removeEventListeners } from './handle-event-listeners';
interface RouteChangeEffect {
scrollTarget: React.MutableRefObject<HTMLElement | null>;
onScroll: () => void;
onTouchStart: () => void;
onTouchMove: () => void;
onTouchEnd: () => void;
callback: () => void;
}
export const useRouteChangeEffect = ({
scrollTarget,
onScroll,
onTouchStart,
onTouchMove,
onTouchEnd,
callback,
}: RouteChangeEffect) => {
const isFetching = useIsFetching();
const isMutating = useIsMutating();
const isLoading = isFetching > 0 || isMutating > 0;
const resetScrollTarget = useCallback(() => {
const rootEl = document.getElementById('root') as HTMLElement;
const pullToRefreshEl = document.getElementsByClassName('ptr__children')[0] as HTMLElement;
scrollTarget.current = rootEl;
if (pullToRefreshEl?.scrollHeight > window.innerHeight) {
scrollTarget.current = pullToRefreshEl;
addEventListeners({ element: pullToRefreshEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
removeEventListeners({ element: rootEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
} else {
addEventListeners({ element: rootEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
removeEventListeners({ element: pullToRefreshEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
}
}, []);
useEffect(() => {
if (isLoading) return;
resetScrollTarget();
}, [isLoading]);
useRouterListener(() => {
setTimeout(() => {
callback?.();
}, 0);
});
};

View File

@@ -1,33 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { MutableRefObject, useEffect } from 'react';
import { addEventListeners, removeEventListeners } from './handle-event-listeners';
interface EventListeners {
scrollTarget: MutableRefObject<HTMLElement | null>;
onScroll: () => void;
onTouchStart: () => void;
onTouchMove: () => void;
onTouchEnd: () => void;
}
export const useScrollEventListeners = ({
scrollTarget,
onScroll,
onTouchStart,
onTouchMove,
onTouchEnd,
}: EventListeners) => {
useEffect(() => {
if(!scrollTarget.current){
return () => {};
}
addEventListeners({ element: scrollTarget.current, onScroll, onTouchStart, onTouchMove, onTouchEnd });
return () => {
const rootEl = document.getElementById('root') as HTMLElement;
const pullToRefreshEl = document.getElementsByClassName('ptr__children')[0] as HTMLElement;
removeEventListeners({ element: rootEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
removeEventListeners({ element: pullToRefreshEl, onScroll, onTouchStart, onTouchMove, onTouchEnd });
};
}, []);
};

View File

@@ -1,22 +0,0 @@
import { useEffect, useRef } from 'react';
export const useScrollTarget = () => {
const scrollTarget = useRef<HTMLElement | null>(null);
useEffect(() => {
const rootEl = document.getElementById('root') as HTMLElement;
const pullToRefreshEl = document.getElementsByClassName('ptr__children')[0] as HTMLElement;
let target: HTMLElement | null = rootEl;
if (pullToRefreshEl?.scrollHeight > window.innerHeight) {
target = pullToRefreshEl;
}
scrollTarget.current = target;
return () => {
scrollTarget.current = null;
};
}, []);
return scrollTarget;
};