Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make floating btn responsive & refactor map #70

Merged
merged 2 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 83 additions & 35 deletions src/app/map/[mapId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import { visitedMapIdsStorage } from '@/utils/storage'
import SearchAnchorBox from './search-anchor-box'
import KorrkKakaoMap from '@/components/korrk-kakao-map'
import { api } from '@/utils/api'
import { PlaceType } from '@/types/api/place'
import type { PlaceType } from '@/types/api/place'
import { notify } from '@/components/common/custom-toast'
import { useIsomorphicLayoutEffect } from '@/hooks/use-isomorphic-layout-effect'
import PlaceListBottomSheet from './place-list-bottom-sheet'
import BottomModal from '@/components/BottomModal'
import { MapDataType } from '@/types/api/maps'
import FilterModalBody, { CategoryType } from './filter-modal-body'
import FilterModalBody, { type CategoryType } from './filter-modal-body'
import useMeasure from '@/hooks/use-measure'
import PlaceMapPopup from '@/components/place/place-map-popup'
import BottomSheet from '@/components/bottom-sheet'
import { APIError, BOTTOM_SHEET_STATE } from '@/models/interface'

export interface FilterIdsType {
category: string[]
Expand All @@ -31,15 +34,36 @@ const MapMain = ({ params: { mapId } }: { params: { mapId: string } }) => {
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false)
const [selectedFilterIds, setSelcectedFilterIds] =
useState<FilterIdsType>(INITIAL_FILTER_IDS)

const [places, setPlaces] = useState<PlaceType[]>([])
const [filteredPlace, setFilteredPlace] = useState<PlaceType[]>([])
const [mapData, setMapData] = useState<MapDataType | null>(null)
const [selectedPlace, setSelectedPlace] = useState<PlaceType | null>(null)

const [mapname, setMapname] = useState<string>('')

const [bottomRef, bottomBounds] = useMeasure()

const visitedMapIds = useMemo(
() => visitedMapIdsStorage.getValueOrNull() ?? [],
[],
)

const handleClickPlace = (place: PlaceType) => () => {
if (selectedPlace?.place.id === place.place.id) {
setSelectedPlace(null)
return
}
setSelectedPlace(place)
}

const handleFilterModalOpen = () => {
setIsFilterModalOpen(!isFilterModalOpen)
}

const resetFilter = () => {
setSelcectedFilterIds(INITIAL_FILTER_IDS)
}

const handleSelectedFilterChange = (value: CategoryType | number) => {
if (value === 'all') {
setSelcectedFilterIds((prev) => ({ ...prev, category: [] }))
Expand Down Expand Up @@ -82,20 +106,24 @@ const MapMain = ({ params: { mapId } }: { params: { mapId: string } }) => {
const { data: placeList } = await api.place.mapId.get(mapId)
setPlaces(placeList)
} catch (err) {
notify.error('예상치 못한 오류가 발생했습니다.')
if (err instanceof APIError) {
notify.error(err.message)
}
}
}

const getMapData = async () => {
const getMapname = async () => {
try {
const { data } = await api.maps.id.get(mapId)
setMapData(data)
setMapname(data.name)
} catch (err) {
notify.error('오류가 발생했습니다.')
if (err instanceof APIError) {
notify.error(err.message)
}
}
}

getMapData()
getMapname()
getPlaceList()
}, [])

Expand Down Expand Up @@ -135,7 +163,7 @@ const MapMain = ({ params: { mapId } }: { params: { mapId: string } }) => {
<div className="w-full flex justify-between">
{/* TODO: 초대장 페이지 제작 후 연결 */}
<Link href="" className="flex items-center">
<Typography size="h3">{mapData?.name ?? ''}</Typography>
<Typography size="h3">{mapname}</Typography>
<Icon type="caretDown" size="lg" />
</Link>
<Link href="/setting">
Expand All @@ -156,35 +184,55 @@ const MapMain = ({ params: { mapId } }: { params: { mapId: string } }) => {
<SearchAnchorBox mapId={mapId} />
</Tooltip>
</header>

<KorrkKakaoMap
bottomBodyElement={
<PlaceListBottomSheet
places={filteredPlace}
selectedFilter={selectedFilterIds}
onClickFilterButton={() => setIsFilterModalOpen(true)}
/>
}
places={filteredPlace}
selectedPlace={selectedPlace}
handleClickPlace={handleClickPlace}
topOfBottomBounds={bottomBounds.top}
/>
<BottomModal
title="보고 싶은 맛집을 선택해주세요"
body={
<FilterModalBody
mapId={mapId}
selectedFilterIds={selectedFilterIds}
onChangeSelectedFilterIds={handleSelectedFilterChange}

{selectedPlace === null ? (
<>
<BottomSheet
ref={bottomRef}
body={
<PlaceListBottomSheet
places={places}
selectedFilter={selectedFilterIds}
onClickFilterButton={handleFilterModalOpen}
/>
}
state={
places.length
? BOTTOM_SHEET_STATE.Default
: BOTTOM_SHEET_STATE.Collapsed
}
/>
}
isOpen={isFilterModalOpen}
cancelMessage="초기화"
confirmMessage="적용"
onClose={() => setIsFilterModalOpen(false)}
onConfirm={() => setIsFilterModalOpen(false)}
onCancel={() => {
setSelcectedFilterIds(INITIAL_FILTER_IDS)
setIsFilterModalOpen(false)
}}
/>
<BottomModal
title="보고 싶은 맛집을 선택해주세요"
body={
<FilterModalBody
mapId={mapId}
selectedFilterIds={selectedFilterIds}
onChangeSelectedFilterIds={handleSelectedFilterChange}
/>
}
isOpen={isFilterModalOpen}
cancelMessage="초기화"
confirmMessage="적용"
onClose={handleFilterModalOpen}
onConfirm={handleFilterModalOpen}
onCancel={resetFilter}
/>
</>
) : (
<PlaceMapPopup
ref={bottomRef}
className="absolute bottom-5 px-5"
selectedPlace={selectedPlace}
/>
)}
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/map/[mapId]/place-list-bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const PlaceListBottomSheet = ({
const userLocation = useUserGeoLocation()

return (
<div className="flex flex-col pt-3.5 px-5">
<div className="flex flex-col px-5">
<div>
<FilterButton
numOfSelectedFilter={
Expand Down
14 changes: 8 additions & 6 deletions src/components/bottom-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ const BottomSheet = forwardRef<HTMLDivElement, BottomSheetProps>(

const [contentRef, contentBounds] = useMeasure()
const dragControls = useDragControls()
const size = useWindowSize()
const { height: windowHeight } = useWindowSize()

const headerHeight = 38
const headerHeight = 36
const defaultHeight = Math.min(
contentBounds.height + headerHeight,
size.height / 2,
windowHeight / 2,
)
const expandedHeight = Math.min(
contentBounds.height + headerHeight,
size.height - headerHeight,
(windowHeight * 3) / 4,
)

const bodyHeight = useMemo(() => {
switch (bottomSheetState) {
case BOTTOM_SHEET_STATE.Expanded:
return expandedHeight - headerHeight
case BOTTOM_SHEET_STATE.Collapsed:
return 0
default:
return defaultHeight - headerHeight
}
Expand Down Expand Up @@ -110,13 +112,13 @@ const BottomSheet = forwardRef<HTMLDivElement, BottomSheetProps>(
aria-expanded={bottomSheetState !== BOTTOM_SHEET_STATE.Collapsed}
>
{/* header */}
<div className="pt-[16px] cursor-grab">
<div className="pt-[16px] pb-[14px] cursor-grab">
{/* bar */}
<div className="w-[53px] h-[6px] bg-[#6D717A] my-0 mx-auto rounded-full" />
</div>
{/* body */}
<div
className="transition-all select-none overflow-y-scroll overscroll-contain no-scrollbar"
className="transition-all duration-300 select-none overflow-y-scroll overscroll-contain no-scrollbar"
style={{ height: bodyHeight }}
aria-hidden={bottomSheetState === BOTTOM_SHEET_STATE.Collapsed}
>
Expand Down
123 changes: 55 additions & 68 deletions src/components/kakao-map/gps-button.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,72 @@
import { forwardRef, useEffect, useState } from 'react'
import type { RefObject } from 'react'
import { useEffect, useState } from 'react'
import { AccessibleIconButton } from '@/components'
import { ClassName } from '@/models/interface'
import useWindowSize from '@/hooks/use-window-size'
import { useKakaoMap } from './context'
import GpsMarker from './gps-marker'
import useUserGeoLocation from '@/hooks/use-user-geo-location'

interface GpsButtonProps extends ClassName {
bottomRef?: RefObject<HTMLDivElement>
const BUTTON_OFFSET_Y = 16
const BUTTON_HEIGHT = 11
interface GpsButtonProps {
topOfBottomBounds: number
}

const DEFAULT_BUTTON_BOTTOM = 16
const GpsButton = ({ topOfBottomBounds }: GpsButtonProps) => {
const userLocation = useUserGeoLocation()
const [gpsBottomPositionY, setGpsBottomPositionY] = useState(BUTTON_OFFSET_Y)
const [gpsMode, setGpsMode] = useState(false)
const { height: windowHeight } = useWindowSize()
const { map } = useKakaoMap()

const GpsButton = forwardRef<HTMLButtonElement, GpsButtonProps>(
({ bottomRef }, ref) => {
const userLocation = useUserGeoLocation()
const [gpsBottomPosition, setGpsBottomPosition] = useState(
DEFAULT_BUTTON_BOTTOM,
)
const [gpsMode, setGpsMode] = useState(false)
const { height } = useWindowSize()
const { map } = useKakaoMap()
const handleGpsClick = () => {
if (!map) return

const handleGpsClick = () => {
if (!map) return

if (!gpsMode) {
const location = new window.kakao.maps.LatLng(
userLocation.latitude,
userLocation.longitude,
)
map.setCenter(location)
}

setGpsMode((prev) => !prev)
if (!gpsMode) {
const location = new window.kakao.maps.LatLng(
userLocation.latitude,
userLocation.longitude,
)
map.setCenter(location)
}

useEffect(() => {
const getGpsButtonPositionY = () => {
if (bottomRef?.current) {
setGpsBottomPosition(
height -
bottomRef.current.getBoundingClientRect().top +
DEFAULT_BUTTON_BOTTOM,
)
return
}

setGpsBottomPosition(DEFAULT_BUTTON_BOTTOM)
}
setGpsMode((prev) => !prev)
}

getGpsButtonPositionY()
}, [bottomRef, height])
useEffect(() => {
if (topOfBottomBounds) {
setGpsBottomPositionY(
Math.min(
windowHeight - topOfBottomBounds + BUTTON_OFFSET_Y,
(windowHeight * 3) / 4 - BUTTON_OFFSET_Y - BUTTON_HEIGHT / 2,
),
)
} else {
setGpsBottomPositionY(BUTTON_OFFSET_Y)
}
}, [topOfBottomBounds, windowHeight])

return (
<>
{gpsMode && (
<GpsMarker
latitude={userLocation.latitude}
longitude={userLocation.longitude}
/>
)}
<AccessibleIconButton
ref={ref}
className={`absolute right-5 transition-[bottom] z-10`}
style={{
bottom: `${gpsBottomPosition}px`,
}}
label={gpsMode ? '내 위치로 이동 취소' : '내 위치로 이동'}
icon={{
className: 'w-11 h-11',
type: gpsMode ? 'locationOn' : 'locationOff',
}}
onClick={handleGpsClick}
return (
<>
{gpsMode && (
<GpsMarker
latitude={userLocation.latitude}
longitude={userLocation.longitude}
/>
</>
)
},
)

GpsButton.displayName = 'GpsButton'
)}
<AccessibleIconButton
className={`absolute right-5 z-10 transition-all ease-in-out duration-300`}
style={{
bottom: `${gpsBottomPositionY}px`,
}}
label={gpsMode ? '내 위치로 이동 취소' : '내 위치로 이동'}
icon={{
className: 'w-11 h-11',
type: gpsMode ? 'locationOn' : 'locationOff',
}}
onClick={handleGpsClick}
/>
</>
)
}

export default GpsButton
Loading
Loading