diff --git a/apps/admin/src/app/requests/[id]/RequestDetailPage.tsx b/apps/admin/src/app/requests/[id]/RequestDetailPage.tsx index 2a704e0..3d567d4 100644 --- a/apps/admin/src/app/requests/[id]/RequestDetailPage.tsx +++ b/apps/admin/src/app/requests/[id]/RequestDetailPage.tsx @@ -8,7 +8,7 @@ import { Header } from '@repo/ui/components/Header' import { Icon } from '@repo/ui/components/Icon' import { Text } from '@repo/ui/components/Text' import { Column, VerticalScrollArea } from '@repo/ui/components/Layout' -import { Banner } from '@repo/ui/components/Banner' +import { Carousel } from '@repo/ui/components/Carousel' import type { RequestDetail } from './_api/types' import { CLIENT_PATH } from '@/consts/path' @@ -56,8 +56,8 @@ export const RequestDetailPage = ({ data }: Props) => { /> {photos.length > 0 && ( - ( + + {photos.map((photo) => ( { className={'max-h-[180px] object-contain'} /> ))} - minHeight={180} - showIndicator={true} - /> + )}
diff --git a/apps/web/app/(home)/page.tsx b/apps/web/app/(home)/page.tsx index 575dd11..b85b669 100644 --- a/apps/web/app/(home)/page.tsx +++ b/apps/web/app/(home)/page.tsx @@ -7,8 +7,8 @@ import { Flex, VerticalScrollArea } from '@repo/ui/components/Layout' import { Icon } from '@repo/ui/components/Icon' import { Text } from '@repo/ui/components/Text' import { Divider } from '@repo/ui/components/Divider' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' -import { Banner } from '@repo/ui/components/Banner' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' +import { Carousel } from '@repo/ui/components/Carousel' import { Categories } from '@/_components/Categories' import { BottomNavigation } from '@/_components/BottomNavigation' import { RankingSection } from './_components/RankingSection' @@ -47,12 +47,10 @@ export default function Page() { > - , - , - ]} - /> + + + + any>( + func: T, + delay: number = 300, +) => { + const timeoutRef = useRef | null>(null) + + const funcRef = useRef(func) + + useEffect(() => { + funcRef.current = func + }, [func]) + + const trigger = useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + timeoutRef.current = setTimeout(async () => { + try { + // 최신 함수 호출 + await funcRef.current(...args) + } catch (error) { + console.error('Debounced function failed:', error) + } + }, delay) + }, + [delay], + ) + + // 언마운트 시 타이머 클리어 + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + return trigger +} diff --git a/apps/web/app/HeroProvider.tsx b/apps/web/app/_providers/HeroProvider.tsx similarity index 100% rename from apps/web/app/HeroProvider.tsx rename to apps/web/app/_providers/HeroProvider.tsx diff --git a/apps/web/app/NaverMapProvider.tsx b/apps/web/app/_providers/NaverMapProvider.tsx similarity index 100% rename from apps/web/app/NaverMapProvider.tsx rename to apps/web/app/_providers/NaverMapProvider.tsx diff --git a/apps/web/app/QueryClientProvider.tsx b/apps/web/app/_providers/QueryClientProvider.tsx similarity index 100% rename from apps/web/app/QueryClientProvider.tsx rename to apps/web/app/_providers/QueryClientProvider.tsx diff --git a/apps/web/app/categories/[id]/page.tsx b/apps/web/app/categories/[id]/page.tsx index ac8be29..32444e2 100644 --- a/apps/web/app/categories/[id]/page.tsx +++ b/apps/web/app/categories/[id]/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { useCategoryQueries } from '@/_apis/queries/category' import { CategoryDetailPage } from '@/categories/[id]/CategoryDetailPage' import { getCategories } from '@/_apis/services/category' diff --git a/apps/web/app/events/food-slot/page.tsx b/apps/web/app/events/food-slot/page.tsx index 9cb3e4c..3ff7577 100644 --- a/apps/web/app/events/food-slot/page.tsx +++ b/apps/web/app/events/food-slot/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { useCategoryQueries } from '@/_apis/queries/category' import FoodSlotMachine from './FoodSlotMachine' import { Header } from '@repo/ui/components/Header' diff --git a/apps/web/app/events/lucky-draw/page.tsx b/apps/web/app/events/lucky-draw/page.tsx index f0b218e..8251941 100644 --- a/apps/web/app/events/lucky-draw/page.tsx +++ b/apps/web/app/events/lucky-draw/page.tsx @@ -4,7 +4,7 @@ import { Flex } from '@repo/ui/components/Layout' import { Icon } from '@repo/ui/components/Icon' import { Text } from '@repo/ui/components/Text' import { LuckyDraw } from './LuckyDraw' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { useEventQueries } from '@/_apis/queries/event' import { InfoPopover } from './_components/InfoPopover' diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index ab6ac51..1e4897c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,17 +1,17 @@ import '@repo/ui/styles.css' import './globals.css' +import { Suspense } from 'react' import Script from 'next/script' import type { Metadata } from 'next' -import { Suspense } from 'react' -import QueryProvider from './QueryClientProvider' import localFont from 'next/font/local' +import { GoogleAnalytics } from '@next/third-parties/google' +import QueryProvider from '@/_providers/QueryClientProvider' +import { NaverMapProvider } from '@/_providers/NaverMapProvider' +import { HeroProvider } from '@/_providers/HeroProvider' +import { CampusInitializer } from '@/_components/CampusInitializer' +import { Column } from '@repo/ui/components/Layout' // import { initServerMSW } from '@/_mocks/initMSW' // import { MSWProvider } from '@/_mocks/MSWProvider' -import { Column } from '@repo/ui/components/Layout' -import { NaverMapProvider } from '@/NaverMapProvider' -import { HeroProvider } from '@/HeroProvider' -import { CampusInitializer } from '@/CampusInitializer' -import { GoogleAnalytics } from '@next/third-parties/google' const SITE_URL = new URL(process.env.NEXT_PUBLIC_CLIENT_URL || '') diff --git a/apps/web/app/likes/page.tsx b/apps/web/app/likes/page.tsx index 0b6b07f..a97697e 100644 --- a/apps/web/app/likes/page.tsx +++ b/apps/web/app/likes/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next' import { usePlaceQueries } from '@/_apis/queries/place' import { OnlyLeftHeader } from '@repo/ui/components/Header' import { VerticalScrollArea } from '@repo/ui/components/Layout' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { LikePlacesList } from './_components/LikePlacesList' import { BottomNavigation } from '@/_components/BottomNavigation' diff --git a/apps/web/app/map/MapComponent.tsx b/apps/web/app/map/MapComponent.tsx index c1156fc..5c92b89 100644 --- a/apps/web/app/map/MapComponent.tsx +++ b/apps/web/app/map/MapComponent.tsx @@ -7,6 +7,7 @@ import { Container, NaverMap } from 'react-naver-maps' import { CAMPUS_LOCATION } from '@/_constants/campus' import { useCampusStore } from '@/_store/campus' import { useLastMapCenterStore } from '@/_store/lastMapCenter' +import { BOTTOM_OFFSET } from './constants/CurrentLocationButton' import { usePlaceQueries } from '@/_apis/queries/place' import type { MapBounds } from '@/_apis/schemas/place' @@ -17,14 +18,15 @@ import { PlaceList } from './_components/PlaceList' import { CampusButtonBox } from './_components/CampusButtom' import { UserMarker, PlaceMarker } from './_components/Marker' import { CurrentLocationButton } from './_components/CurrentLocationButton' -import { PreviewPlace } from './_components/PreviewPlace' +import { PlaceSummaryCard } from './_components/PlaceSummaryCard' import { RefreshButton } from './_components/RefreshButton' +import { useDebounced } from '@/_hooks/useDebounced' const MapComponent = () => { const [map, setMap] = useState(null) const [isCenteredOnUser, setIsCenteredOnUser] = useState(false) const [currentBounds, setCurrentBounds] = useState(null) - const [previewPlaceId, setPreviewPlaceId] = useState(null) + const [selectedPlaceId, setSelectedPlaceId] = useState(null) const [showUpdateButton, setShowUpdateButton] = useState(false) const { campus } = useCampusStore() @@ -33,8 +35,8 @@ const MapComponent = () => { const { data = [] } = useQuery(usePlaceQueries.byMap(currentBounds)) const defaultCenter = toLatLng(lastMapCenter || CAMPUS_LOCATION[campus]) - const previewPlace = previewPlaceId - ? data.find((place) => place.placeId === previewPlaceId)! + const selectedPlace = selectedPlaceId + ? data.find((place) => place.placeId === selectedPlaceId)! : null const refreshMapBounds = useCallback(() => { @@ -66,18 +68,11 @@ const MapComponent = () => { setIsCenteredOnUser(false) } - const onCenterChanged = () => { + const onCenterChanged = useDebounced(() => { setIsCenteredOnUser(false) setShowUpdateButton(true) - } - - const handlePreviewPlace = (placeId: string) => { - setPreviewPlaceId(placeId) - } - - const resetPreviewPlace = () => { - setPreviewPlaceId(null) - } + setSelectedPlaceId(null) + }, 200) useEffect(refreshMapBounds, [refreshMapBounds]) useEffect(() => { @@ -97,21 +92,18 @@ const MapComponent = () => { - + {userLocation && } {data.map((place) => ( @@ -119,15 +111,15 @@ const MapComponent = () => { key={place.placeId} position={place.location} icon={place.categories[0]?.iconKey || 'logo'} - handlePreviewPlace={() => { - handlePreviewPlace(place.placeId) + onClick={() => { + setSelectedPlaceId(place.placeId) }} /> ))} - {previewPlace ? ( - + {selectedPlace ? ( + ) : ( )} diff --git a/apps/web/app/map/_components/CurrentLocationButton/CurrentLocationButton.tsx b/apps/web/app/map/_components/CurrentLocationButton/CurrentLocationButton.tsx index 8d82e88..d8729b7 100644 --- a/apps/web/app/map/_components/CurrentLocationButton/CurrentLocationButton.tsx +++ b/apps/web/app/map/_components/CurrentLocationButton/CurrentLocationButton.tsx @@ -3,22 +3,23 @@ import { motion } from 'motion/react' import { Icon } from '@repo/ui/components/Icon' import { cn } from '@repo/ui/utils/cn' +import { BOTTOM_OFFSET } from '@/map/constants/CurrentLocationButton' type Props = { onClick: VoidFunction isCenteredOnUser: boolean - previewPlaceId: string | null + bottomOffset?: number } -const windowHeight = Math.floor(window.innerHeight * 0.2) + 10 + export const CurrentLocationButton = ({ onClick, isCenteredOnUser, - previewPlaceId, + bottomOffset, }: Props) => { return ( { export const PlaceMarker = ({ icon, position, - handlePreviewPlace = () => {}, + onClick = () => {}, }: { icon: IconType position: Coord - handlePreviewPlace?: VoidFunction + onClick?: VoidFunction }) => { const naverMaps = useNavermaps() const MarkerIcon = ReactDOMServer.renderToString( @@ -54,7 +54,7 @@ export const PlaceMarker = ({ { e.pointerEvent.stopPropagation() - handlePreviewPlace() + onClick() }} position={new naverMaps.LatLng(toLatLng(position))} icon={{ diff --git a/apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx b/apps/web/app/map/_components/PlaceSummaryCard/PlaceSummaryCard.tsx similarity index 96% rename from apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx rename to apps/web/app/map/_components/PlaceSummaryCard/PlaceSummaryCard.tsx index d96dd08..c24aeec 100644 --- a/apps/web/app/map/_components/PreviewPlace/PreviewPlace.tsx +++ b/apps/web/app/map/_components/PlaceSummaryCard/PlaceSummaryCard.tsx @@ -8,7 +8,7 @@ import { Text } from '@repo/ui/components/Text' import { Icon } from '@repo/ui/components/Icon' import { Column, Flex } from '@repo/ui/components/Layout' -export const PreviewPlace = ({ place }: { place: PlaceByMap }) => { +export const PlaceSummaryCard = ({ place }: { place: PlaceByMap }) => { const { placeId, placeName, categories, address, photos } = place const mainCategoryIcon = categories[0]?.iconKey || 'logo' diff --git a/apps/web/app/map/_components/PlaceSummaryCard/index.tsx b/apps/web/app/map/_components/PlaceSummaryCard/index.tsx new file mode 100644 index 0000000..5bbe565 --- /dev/null +++ b/apps/web/app/map/_components/PlaceSummaryCard/index.tsx @@ -0,0 +1 @@ +export { PlaceSummaryCard } from './PlaceSummaryCard' diff --git a/apps/web/app/map/_components/PreviewPlace/index.tsx b/apps/web/app/map/_components/PreviewPlace/index.tsx deleted file mode 100644 index e733d72..0000000 --- a/apps/web/app/map/_components/PreviewPlace/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { PreviewPlace } from './PreviewPlace' diff --git a/apps/web/app/map/constants/CurrentLocationButton.ts b/apps/web/app/map/constants/CurrentLocationButton.ts new file mode 100644 index 0000000..d8626e5 --- /dev/null +++ b/apps/web/app/map/constants/CurrentLocationButton.ts @@ -0,0 +1,7 @@ +const SPACE_FROM_BOTTOM_SHEET = 10 + +export const BOTTOM_OFFSET = { + WITH_BOTTOM_SHEET: + Math.floor(window.innerHeight * 0.2) + SPACE_FROM_BOTTOM_SHEET, + WITH_SUMMARY_CARD: 220, +} as const diff --git a/apps/web/app/places/[id]/PlaceDetailPage.tsx b/apps/web/app/places/[id]/PlaceDetailPage.tsx index 8dffe2a..5e1e34f 100644 --- a/apps/web/app/places/[id]/PlaceDetailPage.tsx +++ b/apps/web/app/places/[id]/PlaceDetailPage.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useCampusStore } from '@/_store/campus' import { PlaceQueryKeys, usePlaceQueries } from '@/_apis/queries/place' -import { Banner } from '@repo/ui/components/Banner' +import { Carousel } from '@repo/ui/components/Carousel' import { HeaderBackButton } from '@/_components/HeaderBackButton' import { Text } from '@repo/ui/components/Text' import { Header } from '@repo/ui/components/Header' @@ -47,8 +47,8 @@ export const PlaceDetailPage = ({ id }: { id: string }) => { right={} /> - ( + + {photos.map((photo, index) => ( { priority={index === 0} /> ))} - minHeight={180} - showIndicator={true} - /> +
diff --git a/apps/web/app/places/[id]/page.tsx b/apps/web/app/places/[id]/page.tsx index 4a9dfca..5d52209 100644 --- a/apps/web/app/places/[id]/page.tsx +++ b/apps/web/app/places/[id]/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' import { usePlaceQueries } from '@/_apis/queries/place' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { PlaceDetailPage } from './PlaceDetailPage' import { getPlaceDetail } from '@/_apis/services/place' diff --git a/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx b/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx index 7641519..428d8ba 100644 --- a/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx +++ b/apps/web/app/places/new/_components/Step/PlacePreview/PlacePreview.tsx @@ -3,7 +3,7 @@ import { useSuspenseQuery } from '@tanstack/react-query' import type { UseFormGetValues, UseFormSetValue } from 'react-hook-form' import type { NewPlaceRequest } from '@/_apis/schemas/place' import { usePlaceQueries } from '@/_apis/queries/place' -import { Banner } from '@repo/ui/components/Banner' +import { Carousel } from '@repo/ui/components/Carousel' import { Location, Menus } from '@/places/[id]/_components' import { Text } from '@repo/ui/components/Text' import { Button } from '@repo/ui/components/Button' @@ -51,8 +51,8 @@ export const PlacePreview = ({ getValues, setValue, nextStep }: Props) => { - ( + + {photos.map((photo) => ( { className={'max-h-[180px] object-contain'} /> ))} - showIndicator={true} - minHeight={180} - /> +
diff --git a/apps/web/app/profile/page.tsx b/apps/web/app/profile/page.tsx index b9b0b42..b83e27c 100644 --- a/apps/web/app/profile/page.tsx +++ b/apps/web/app/profile/page.tsx @@ -1,6 +1,6 @@ import { CLIENT_PATH } from '@/_constants/path' import { useUserQueries } from '@/_apis/queries/user' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { BottomNavigation } from '@/_components/BottomNavigation' import { OnlyLeftHeader } from '@repo/ui/components/Header' import { Column } from '@repo/ui/components/Layout' diff --git a/apps/web/app/requests/[id]/RequestDetailPage.tsx b/apps/web/app/requests/[id]/RequestDetailPage.tsx index 9023b26..8edcf67 100644 --- a/apps/web/app/requests/[id]/RequestDetailPage.tsx +++ b/apps/web/app/requests/[id]/RequestDetailPage.tsx @@ -7,7 +7,7 @@ import { useRequestQueries } from '@/_apis/queries/request' import { Header } from '@repo/ui/components/Header' import { Text } from '@repo/ui/components/Text' import { Column, VerticalScrollArea } from '@repo/ui/components/Layout' -import { Banner } from '@repo/ui/components/Banner' +import { Carousel } from '@repo/ui/components/Carousel' import { HeaderBackButton } from '@/_components/HeaderBackButton' import { Description, Location, Menus, Tags } from '@/places/[id]/_components' import { StatusChip } from '@/requests/_components/StatusChip' @@ -42,8 +42,8 @@ export const RequestDetailPage = ({ id }: { id: string }) => { {rejectedReason && registerStatus === 'REJECTED' && ( )} - ( + + {photos.map((photo) => ( { className={'max-h-[180px] object-contain'} /> ))} - minHeight={180} - showIndicator={true} - /> +
diff --git a/apps/web/app/requests/[id]/page.tsx b/apps/web/app/requests/[id]/page.tsx index 7f21b84..3716a50 100644 --- a/apps/web/app/requests/[id]/page.tsx +++ b/apps/web/app/requests/[id]/page.tsx @@ -1,5 +1,5 @@ import { RequestDetailPage } from './RequestDetailPage' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { useRequestQueries } from '@/_apis/queries/request' export const dynamic = 'force-dynamic' diff --git a/apps/web/app/requests/page.tsx b/apps/web/app/requests/page.tsx index 2a3a3ac..bac3f2e 100644 --- a/apps/web/app/requests/page.tsx +++ b/apps/web/app/requests/page.tsx @@ -5,7 +5,7 @@ import { Icon } from '@repo/ui/components/Icon' import { Text } from '@repo/ui/components/Text' import { useRequestQueries } from '@/_apis/queries/request' -import { HydrationBoundaryPage } from '@/HydrationBoundaryPage' +import { HydrationBoundaryPage } from '@/_components/HydrationBoundaryPage' import { RequestPlacesList } from './_components/RequestPlacesList' export const dynamic = 'force-dynamic' diff --git a/packages/ui/src/components/Banner/index.tsx b/packages/ui/src/components/Banner/index.tsx deleted file mode 100644 index 1a83a85..0000000 --- a/packages/ui/src/components/Banner/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { Banner } from './Banner' diff --git a/packages/ui/src/components/Banner/Banner.tsx b/packages/ui/src/components/Carousel/Carousel.tsx similarity index 87% rename from packages/ui/src/components/Banner/Banner.tsx rename to packages/ui/src/components/Carousel/Carousel.tsx index 842feec..2ed0c06 100644 --- a/packages/ui/src/components/Banner/Banner.tsx +++ b/packages/ui/src/components/Carousel/Carousel.tsx @@ -1,49 +1,46 @@ 'use client' -import { useState } from 'react' +import { useState, type ReactNode } from 'react' import 'keen-slider/keen-slider.min.css' import { useKeenSlider } from 'keen-slider/react' import { cn } from '@repo/ui/utils/cn' type Props = { - contents: React.ReactNode[] minHeight?: number showIndicator?: boolean + children: ReactNode[] } /** - * Banner 컴포넌트 + * Carousel 컴포넌트 * * - 여러 콘텐츠를 순차적으로 보여주는 슬라이더 배너입니다. * - `keen-slider`를 기반으로 자동 재생(loop) 기능을 제공합니다. * - 마우스를 올리면 자동 재생이 일시 정지되고, 마우스를 치우면 다시 재생됩니다. * - * @param contents 렌더링할 React 노드 배열 (각각의 배너 콘텐츠) + * @param children 렌더링할 React 노드 배열 (각각의 배너 콘텐츠) * @param minHeight 배너의 최소 높이(px). 기본값은 150입니다. * @param showIndicator 인디케이터 노출 유무. 기본값은 false 입니다. * * @example * ```tsx - * 배너 1, - *
배너 2
, - *
배너 3
, - * ]} - * minHeight={200} - * /> + * + *
배너 1
+ *
배너 2
+ *
배너 3
+ *
* ``` */ -export const Banner = ({ - contents, +export const Carousel = ({ minHeight = 150, showIndicator = false, + children, }: Props) => { const [currentSlide, setCurrentSlide] = useState(0) const [loaded, setLoaded] = useState(false) const [sliderRef, instanceRef] = useKeenSlider( { - loop: contents.length > 1, + loop: children.length > 1, initial: 0, slideChanged(slider) { setCurrentSlide(slider.track.details.rel) @@ -90,7 +87,7 @@ export const Banner = ({ ], ) - if (contents.length === 0) { + if (children.length === 0) { return null } @@ -100,7 +97,7 @@ export const Banner = ({ className={'keen-slider ui:relative'} style={{ minHeight }} > - {contents.map((content, index) => ( + {children.map((content, index) => (