diff --git a/src/app/router/Router.tsx b/src/app/router/Router.tsx index 41a51031..dc9bd3f1 100644 --- a/src/app/router/Router.tsx +++ b/src/app/router/Router.tsx @@ -7,7 +7,7 @@ import { AroundSearch, TourList, TourSearch, - TourSingleInfo, + TourShare, } from '@/app/router'; import { Home } from '@/pages/home'; import { GeoTrip } from '@/pages/tour/geotrip'; @@ -28,9 +28,8 @@ export default function Router() { } /> }> - } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/app/router/lazyRoutes.ts b/src/app/router/lazyRoutes.ts index d20ea25e..4112a1c3 100644 --- a/src/app/router/lazyRoutes.ts +++ b/src/app/router/lazyRoutes.ts @@ -9,6 +9,4 @@ export const AroundSearch = lazy( ); export const TourList = lazy(() => import('@/pages/tour/tourList/TourList')); export const TourSearch = lazy(() => import('@/pages/tour/search/TourSearch')); -export const TourSingleInfo = lazy( - () => import('@/pages/tour/geotrip/SingleTrip'), -); +export const TourShare = lazy(() => import('@/pages/tour/geotrip/Share')); diff --git a/src/features/auth/ui/AuthButtonContainer.tsx b/src/features/auth/ui/AuthButtonContainer.tsx index 59442a78..38fb5c2f 100644 --- a/src/features/auth/ui/AuthButtonContainer.tsx +++ b/src/features/auth/ui/AuthButtonContainer.tsx @@ -10,7 +10,7 @@ export default function AuthButtonContainer() { mutation.mutate(); }; const handleGuestLogin = () => { - navigate('/tour/geo-trip?distance=20000&tour-type=12'); + navigate('/tour/geo'); }; return ( diff --git a/src/features/shared/index.ts b/src/features/shared/index.ts new file mode 100644 index 00000000..03a8d531 --- /dev/null +++ b/src/features/shared/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './ui'; diff --git a/src/features/shared/lib/index.ts b/src/features/shared/lib/index.ts new file mode 100644 index 00000000..2de0abe5 --- /dev/null +++ b/src/features/shared/lib/index.ts @@ -0,0 +1 @@ +export { default as useKakaoShare } from './useKakaoShare'; diff --git a/src/features/tour/ui/single/Single.tsx b/src/features/shared/ui/ShareTrip.tsx similarity index 96% rename from src/features/tour/ui/single/Single.tsx rename to src/features/shared/ui/ShareTrip.tsx index 373a0e74..b901ce25 100644 --- a/src/features/tour/ui/single/Single.tsx +++ b/src/features/shared/ui/ShareTrip.tsx @@ -5,7 +5,7 @@ import { tourQueries, type TourItem } from '@/entities/tour'; import { LoadingSpinner } from '@/shared/ui'; import { TourSlide } from '@/features/tourShort'; -export default function Single() { +export default function ShareTrip() { const { contentId } = useParams(); const navigate = useNavigate(); diff --git a/src/features/shared/ui/SharedButtonContainer.tsx b/src/features/shared/ui/SharedButtonContainer.tsx index 23d3998a..4d32e447 100644 --- a/src/features/shared/ui/SharedButtonContainer.tsx +++ b/src/features/shared/ui/SharedButtonContainer.tsx @@ -9,7 +9,7 @@ interface SharedButtonContainerProps { export default function SharedButtonContainer({ contentId, }: SharedButtonContainerProps) { - const link = `${window.location.origin}/tour/single/${contentId}`; + const link = `${window.location.origin}/tour/share/${contentId}`; const { shareKakao } = useKakaoShare({ contentId, link }); return ( diff --git a/src/features/shared/ui/index.ts b/src/features/shared/ui/index.ts new file mode 100644 index 00000000..a36efd3f --- /dev/null +++ b/src/features/shared/ui/index.ts @@ -0,0 +1,2 @@ +export { default as ShareTrip } from './ShareTrip'; +export { default as SharedButtonContainer } from './SharedButtonContainer'; diff --git a/src/features/tourDetail/ui/TourOverview.tsx b/src/features/tourDetail/ui/TourOverview.tsx index 41cbd17f..997483c1 100644 --- a/src/features/tourDetail/ui/TourOverview.tsx +++ b/src/features/tourDetail/ui/TourOverview.tsx @@ -1,10 +1,9 @@ import { useSuspenseQuery } from '@tanstack/react-query'; -import { commonSVG } from '@/assets'; - import { BookmarkButtonContainer } from '@/features/bookmark'; import { tourQueries } from '@/entities/tour'; -import { TourTypeBadge, DistanceTimeInfo, getCopyClipBoard } from '@/shared'; +import { TourTypeBadge, DistanceTimeInfo } from '@/shared'; +import { SharedButtonContainer } from '@/features/shared'; interface TourCardProps { distance: string; @@ -37,10 +36,8 @@ export default function TourOverview({
- getCopyClipBoard(window.location.href)} - /> + +
diff --git a/src/features/tourShort/hook/index.ts b/src/features/tourShort/hook/index.ts index 57b031a9..8586ad07 100644 --- a/src/features/tourShort/hook/index.ts +++ b/src/features/tourShort/hook/index.ts @@ -1,2 +1,2 @@ export * from './useInfiniteSwiperControl'; -export * from './usePersistSlideUrl'; +export * from './usePersistSlideStorage'; diff --git a/src/features/tourShort/hook/usePersistSlideStorage.ts b/src/features/tourShort/hook/usePersistSlideStorage.ts new file mode 100644 index 00000000..b0f66866 --- /dev/null +++ b/src/features/tourShort/hook/usePersistSlideStorage.ts @@ -0,0 +1,43 @@ +import { useSessionStorage } from '@/shared'; +import { useCallback } from 'react'; + +interface SlideState { + pageParam: number; + index: number; +} + +export const usePersistSlideStorage = () => { + const [slideState, setSlideState] = useSessionStorage( + 'slideState', + { + index: 0, + pageParam: 1, + }, + ); + + const setSlideParams = useCallback( + ({ index, pageParam }: SlideState) => { + setSlideState({ index: index % 10, pageParam }); + }, + [setSlideState], + ); + + const getSlideIndex = () => { + return slideState.index; + }; + + const getPageParam = () => { + return slideState.pageParam; + }; + + const slideReset = () => { + setSlideState({ index: 0, pageParam: 1 }); + }; + + return { + setSlideParams, + getSlideIndex, + getPageParam, + slideReset, + }; +}; diff --git a/src/features/tourShort/hook/usePersistSlideUrl.ts b/src/features/tourShort/hook/usePersistSlideUrl.ts deleted file mode 100644 index 729c109b..00000000 --- a/src/features/tourShort/hook/usePersistSlideUrl.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback } from 'react'; -import { useSearchParams } from 'react-router-dom'; - -interface handleSlideChangeProps { - pageParam: number; - index: number; -} - -export const usePersistSlideUrl = () => { - const [searchParams, setSearchParams] = useSearchParams(); - - const setSlideParams = useCallback( - ({ index, pageParam }: handleSlideChangeProps) => { - searchParams.set('slide-index', (index % 10).toString()); - searchParams.set('page-param', pageParam.toString()); - setSearchParams(searchParams); - }, - [], - ); - - const getSlideIndex = () => { - const slideIndex = searchParams.get('slide-index'); - if (slideIndex === null) { - return 0; - } - return Number(slideIndex); - }; - - const getPageParam = () => { - const pageParam = searchParams.get('page-param'); - if (pageParam === null) { - return 1; - } - return Number(pageParam); - }; - - const slideReset = () => { - const nextParams = new URLSearchParams(searchParams); - nextParams.delete('slide-index'); - nextParams.delete('page-param'); - setSearchParams(nextParams); - }; - - return { - setSlideParams, - getSlideIndex, - getPageParam, - slideReset, - }; -}; diff --git a/src/features/tourShort/ui/TourSwiperContainer.tsx b/src/features/tourShort/ui/TourSwiperContainer.tsx index e950cc5d..b61b62af 100644 --- a/src/features/tourShort/ui/TourSwiperContainer.tsx +++ b/src/features/tourShort/ui/TourSwiperContainer.tsx @@ -10,7 +10,7 @@ import { TourSwiperView, useTourSwiperInfiniteQuery, useInfiniteSwiperControl, - usePersistSlideUrl, + usePersistSlideStorage, } from '@/features/tourShort'; import { getSuspenseLocation } from '@/shared'; @@ -18,7 +18,8 @@ import type { Swiper as SwiperType } from 'swiper/types'; import type { TourInjected } from '@/features/tour'; function TourSwiperContainer({ distance, contentTypeId }: TourInjected) { - const { setSlideParams, getSlideIndex, getPageParam } = usePersistSlideUrl(); + const { setSlideParams, getSlideIndex, getPageParam } = + usePersistSlideStorage(); const geoLocation = getSuspenseLocation(); const { slideEntries, fetchAppend, fetchPrepend } = diff --git a/src/pages/tour/geotrip/SingleTrip.tsx b/src/pages/tour/geotrip/Share.tsx similarity index 80% rename from src/pages/tour/geotrip/SingleTrip.tsx rename to src/pages/tour/geotrip/Share.tsx index 867cc7a5..1de4f26c 100644 --- a/src/pages/tour/geotrip/SingleTrip.tsx +++ b/src/pages/tour/geotrip/Share.tsx @@ -1,24 +1,27 @@ -import SingleTrip from '@/features/tour/ui/single/Single'; -import { LoadingSpinner, QueryErrorBoundary } from '@/shared'; -import { BottomNavigationBar, Seo } from '@/widgets'; import { Suspense } from 'react'; import { useParams } from 'react-router-dom'; -export default function SingleTripPage() { +import { BottomNavigationBar, Seo } from '@/widgets'; + +import { ShareTrip } from '@/features/shared'; + +import { LoadingSpinner, QueryErrorBoundary } from '@/shared'; + +export default function ShareTripPage() { const { contentId } = useParams(); return ( <>
}> - +
diff --git a/src/pages/tour/geotrip/index.ts b/src/pages/tour/geotrip/index.ts index 0797c0b2..20640dc7 100644 --- a/src/pages/tour/geotrip/index.ts +++ b/src/pages/tour/geotrip/index.ts @@ -1,2 +1,2 @@ export { default as GeoTrip } from './GeoTrip'; -export { default as SingleTrip } from './SingleTrip'; +export { default as SingleTrip } from './Share'; diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index c3eb0799..156ada1f 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -1,5 +1,6 @@ export * from './useSyncedState'; export * from './useDebouncedCallback'; export * from './useLocalStorage'; +export * from './useSessionStorage'; export * from './useFunnel'; export * from './useToggleState'; diff --git a/src/shared/hooks/useSessionStorage.ts b/src/shared/hooks/useSessionStorage.ts new file mode 100644 index 00000000..bd777721 --- /dev/null +++ b/src/shared/hooks/useSessionStorage.ts @@ -0,0 +1,56 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +/** + * sessionStorage 값을 저장하고 불러오는 커스텀 훅입니다. + * dispatchEvent를 사용하여 다른 컴포넌트에서 sessionStorage의 변경을 감지할 수 있습니다. + * + * @param {string} key - sessionStorage에서 사용할 키 + * @param {T} initialValue - 초기값 + * @returns {[T, (value: T | ((val: T) => T)) => void]} - 저장된 값과 업데이트 함수를 반환합니다. + * + * @example + * //초기값에 의해, 스토리지에 저장된 정보가 없다면 자동으로 값을 저장합니다. + * const [value, setValue] = useLocalStorage('myKey', 'defaultValue'); + */ +export const useSessionStorage = (key: string, initialValue: T) => { + const readValue = useCallback((): T => { + try { + const item = window.sessionStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + throw new Error(`스토리지를 불러오는데 실패했습니다: ${error}`); + } + }, [key, initialValue]); + + const [storedValue, setStoredValue] = useState(readValue); + const sessionStorageEventRef = useRef(new Event('session-storage')); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = + value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + window.sessionStorage.setItem(key, JSON.stringify(valueToStore)); + + window.dispatchEvent(sessionStorageEventRef.current); + } catch (error) { + console.error(`Error setting sessionStorage key "${key}":`, error); + } + }; + + useEffect(() => { + const handleStorageChange = () => { + setStoredValue(readValue()); + }; + + window.addEventListener('session-storage', handleStorageChange); + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('session-storage', handleStorageChange); + window.removeEventListener('storage', handleStorageChange); + }; + }, [readValue]); + + return [storedValue, setValue] as const; +}; diff --git a/src/widgets/bottomNavigationBar/BottomNavigationBar.test.tsx b/src/widgets/bottomNavigationBar/BottomNavigationBar.test.tsx index 191669d0..39601d12 100644 --- a/src/widgets/bottomNavigationBar/BottomNavigationBar.test.tsx +++ b/src/widgets/bottomNavigationBar/BottomNavigationBar.test.tsx @@ -48,7 +48,7 @@ describe('BottomNavigationBar 컴포넌트', () => { await user.click(homeButton); expect(mockNavigate).toHaveBeenCalledWith( - expect.stringContaining('/tour/geo-trip'), + expect.stringContaining('/tour/geo'), { replace: true }, ); }); diff --git a/src/widgets/bottomNavigationBar/utils.ts b/src/widgets/bottomNavigationBar/utils.ts index fd4332e5..b48607a4 100644 --- a/src/widgets/bottomNavigationBar/utils.ts +++ b/src/widgets/bottomNavigationBar/utils.ts @@ -19,9 +19,9 @@ export const createNavItems = ({ navigate }: createNavItemsParams) => { { id: 'home-navigation-tutorial', icon: HomeIcon, - path: '/tour/geo-trip', + path: '/tour/geo', label: 'home Icon', - onClick: () => navigateTo('/tour/geo-trip'), + onClick: () => navigateTo('/tour/geo'), }, { id: 'list-navigation-tutorial',