From 7dc4a3e450c1fc54d292c9948d9871510a460f56 Mon Sep 17 00:00:00 2001 From: d0dam Date: Mon, 31 Jul 2023 15:10:42 +0900 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 60f870732951b14a824138828074370862fe40a8 Author: Jeremy <102432453+shackstack@users.noreply.github.com> Date: Mon Jul 31 14:41:46 2023 +0900 feat: 레스토랑 카드 및 마커 클릭 이벤트 변경 (#198) * feat: 마커 호버시 마커를 맨 앞으로 가져오기 (#192) * style: 마커 호버시 마커 강조 (#192) * feat: 마커 클릭시 레스토랑 카드 띄우기 (#192) * refactor: 불필요한 코드 제거 (#192) - 레스토랑 카드 클릭시 맵 모달 이벤트 제거 - 마커 클릭시 맵 모달 이벤트 제거 * feat: 마커클릭 시 마커 위치에 따라 카드모달 위치 조정 (#192) * style: 레스토랑 오버레이 스타일 수정 (#192) * refactor: restaurantCard 컴포넌트를 용도에 따라 스타일 다르게 설정 (#192) * feat: 마커 클릭시 강조 효과 주기 (#192) * Squashed commit of the following: commit 3442d16c306a3a1f478a5ee58c9664daa271bf3f Author: Minjae Kim Date: Fri Jul 28 15:06:21 2023 +0900 design: 전체 카테고리에 해당하는 이미지 수정 (#195) (#196) commit 21128038e8dbd0953497950833f65dc918ffc40b Author: 황준승 <78203399+turtle601@users.noreply.github.com> Date: Thu Jul 27 16:45:06 2023 +0900 feat: 음식점 리스트 중복 필터링 기능 구현 (#186) * refactor: 지도 boundary 타입 추가 및 음식점 카테고리 타입 일부 수정 (#180) * feat: 셀럽 및 음식점 카테고리 별 필터링 기능 추가 (#180) * refactor: getQueryString 로직 분리 및 적용 (#180) * feat: CelebDropDown 및 CategoryNavbar에 전체 버튼 추가 (#184) * fix: 필터링 클릭 시 렌더링이 한 박자 늦게되는 오류 해결 (#184) * refactor: Map 컴포넌트에서 사용하지 않는 props 속성 제거 (#184) * refactor: celeb 전체를 나타내는 상태값을 -1로 변경 (#184) * feat: CelebDropDown blur 기능 추가 (#184) * fix: 불필요한 useEffect dependency 제거 (#184) * feat: Restaurant_Category에 전체 옵션 추가 (#184) * fix: CelebId 초기값 수정 (#184) * fix: API 명세서 수정에 따른 데이터 타입 변경 (#184) * refactor: NavButton props 프로퍼티 수정으로 인한 코드 수정 (#184) * refactor: css 선언방식을 삼항연산자를 && 로 변경 (#184) * fix: NavButton 불필요한 hover 기능 제거 (#184) Changed: hover 이벤트 && 연산자를 삼항연산자 사용으로 변경 * refactor: NavButton 컴포넌트를 NavItem 컴포넌트로 네이밍 수정 (#184) --------- Co-authored-by: d0dam Co-authored-by: Jeremy <102432453+shackstack@users.noreply.github.com> * feat: 다른 마커 클릭시 기존 마커 모달 닫기 기능 구현 (#192) * style: 파일명 오류 수정 (#192) * refactor: baseURL 환경변수 설정 및 type import 분리 * feat: RestaurantCard 컴포넌트 props 수정 (#192) onClick을 optional로 수정 * refactor: getQuadrant 리팩터링 (#192) * style: 상태 네이밍 수정 (#192) mainPosition -> currentCenter * feat: 음식점 카드 호버시 해당 음식점 마커 강조 (#192) * design: 강조시 애니메이션 효과 추가 및 음식점 리스트 스타일 수정 (#192) * refactor: 음식점 카드 호버시 마커 강조 로직 변경 (#192) * refactor: 프로필 이미지 컴포넌트 Props 타입 수정 (#192) size: number => string * fix: setHoverId가 없을 때 default value 설정 (#192) --- frontend/src/App.tsx | 2 + frontend/src/components/@common/Map/Map.tsx | 19 +++++---- .../components/@common/Map/OverlayMarker.tsx | 42 ++++++++++++++----- .../ProfileImage/ProfileImage.stories.tsx | 2 +- .../@common/ProfileImage/ProfileImage.tsx | 8 ++-- .../CategoryNavbar/CategoryNavbar.tsx | 4 +- .../components/CelebBanner/CelebBanner.tsx | 2 +- .../CelebDropDown/CelebDropDown.tsx | 2 +- .../MapModalContent/MapModalContent.tsx | 6 +-- .../RestaurantCard/RestaurantCard.stories.tsx | 2 +- .../RestaurantCard/RestaurantCard.tsx | 35 +++++++++++----- .../RestaurantCardList/RestaurantCardList.tsx | 8 ++-- .../components/VideoPreview/VideoPreview.tsx | 2 +- frontend/src/hooks/useOnClickOutside.ts | 19 +++++++++ frontend/src/pages/MainPage.tsx | 14 +++++-- frontend/src/utils/getQuadrant.ts | 4 +- 16 files changed, 118 insertions(+), 53 deletions(-) create mode 100644 frontend/src/hooks/useOnClickOutside.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b45495c4f..902884ad0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,7 @@ import MainPage from './pages/MainPage'; +export const { BASE_URL } = process.env; + function App() { return ; } diff --git a/frontend/src/components/@common/Map/Map.tsx b/frontend/src/components/@common/Map/Map.tsx index d107c2aec..82137b652 100644 --- a/frontend/src/components/@common/Map/Map.tsx +++ b/frontend/src/components/@common/Map/Map.tsx @@ -1,8 +1,6 @@ import { useState } from 'react'; import { Wrapper, Status } from '@googlemaps/react-wrapper'; import { styled } from 'styled-components'; -import OverlayMarker from './OverlayMarker'; -import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; import MapContent from './MapContent'; import OverlayMyLocation from './OverlayMyLocation'; import LoadingDots from '../LoadingDots'; @@ -12,11 +10,15 @@ import LeftBracket from '~/assets/icons/left-bracket.svg'; import RightBracket from '~/assets/icons/right-bracket.svg'; import Minus from '~/assets/icons/minus.svg'; import Plus from '~/assets/icons/plus.svg'; -import { RestaurantData } from '~/@types/api.types'; import getQuadrant from '~/utils/getQuadrant'; +import OverlayMarker from './OverlayMarker'; + +import type { Coordinate, CoordinateBoundary } from '~/@types/map.types'; +import type { RestaurantData } from '~/@types/api.types'; interface MapProps { data: RestaurantData[]; + hoveredId: number | null; setBoundary: React.Dispatch>; toggleMapExpand: () => void; loadingData: boolean; @@ -42,14 +44,16 @@ const StyledMapLoadingContainer = styled.section` background-color: var(--gray-2); `; -function Map({ data, setBoundary, toggleMapExpand, loadingData }: MapProps) { +const JamsilCampus = { lat: 37.515271, lng: 127.1029949 }; + +function Map({ data, setBoundary, toggleMapExpand, loadingData, hoveredId }: MapProps) { const [center, setCenter] = useState({ lat: 37.5057482, lng: 127.050727 }); const [clicks, setClicks] = useState([]); const [zoom, setZoom] = useState(16); const [myPosition, setMyPosition] = useState(null); const [isMapExpanded, setIsMapExpanded] = useState(false); const [loading, setLoading] = useState(false); - const [mainPosition, setMainPosition] = useState({ lat: 37.5057482, lng: 127.050727 }); + const [currentCenter, setCurrentCenter] = useState(JamsilCampus); const onClick = (e: google.maps.MapMouseEvent) => { setClicks([...clicks, e.latLng!]); @@ -57,7 +61,7 @@ function Map({ data, setBoundary, toggleMapExpand, loadingData }: MapProps) { const onIdle = (m: google.maps.Map) => { setZoom(m.getZoom()!); - setMainPosition({ lat: m.getCenter().lat(), lng: m.getCenter().lng() }); + setCurrentCenter({ lat: m.getCenter().lat(), lng: m.getCenter().lng() }); const lowLatitude = String(m.getBounds().getSouthWest().lat()); const highLatitude = String(m.getBounds().getNorthEast().lat()); @@ -103,7 +107,8 @@ function Map({ data, setBoundary, toggleMapExpand, loadingData }: MapProps) { ); })} diff --git a/frontend/src/components/@common/Map/OverlayMarker.tsx b/frontend/src/components/@common/Map/OverlayMarker.tsx index 8a2a45a24..414ffdd7c 100644 --- a/frontend/src/components/@common/Map/OverlayMarker.tsx +++ b/frontend/src/components/@common/Map/OverlayMarker.tsx @@ -1,35 +1,39 @@ -import styled, { keyframes } from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; import { useRef, useState } from 'react'; import ProfileImage from '../ProfileImage'; import Overlay from './Overlay/Overlay'; -import type { Celeb } from '~/@types/celeb.types'; -import { Restaurant } from '~/@types/restaurant.types'; import RestaurantCard from '~/components/RestaurantCard'; +import useOnClickOutside from '~/hooks/useOnClickOutside'; + import type { Quadrant } from '~/utils/getQuadrant'; -import useOnClickOutside from '~/hooks/useOnClickOuside'; +import type { Restaurant } from '~/@types/restaurant.types'; +import type { Celeb } from '~/@types/celeb.types'; interface OverlayMarkerProps { celeb: Celeb; map?: google.maps.Map; restaurant: Restaurant; quadrant: Quadrant; + isRestaurantHovered: boolean; } -function OverlayMarker({ celeb, restaurant, map, quadrant }: OverlayMarkerProps) { +function OverlayMarker({ celeb, restaurant, map, quadrant, isRestaurantHovered }: OverlayMarkerProps) { const { lat, lng } = restaurant; const [isClicked, setIsClicked] = useState(false); const ref = useRef(); useOnClickOutside(ref, () => setIsClicked(false)); + const clickMarker = () => setIsClicked(true); + return ( map && ( - - setIsClicked(true)} isClicked={isClicked} ref={ref}> - + + + {isClicked && ( - {}} type="map" /> + )} @@ -37,7 +41,16 @@ function OverlayMarker({ celeb, restaurant, map, quadrant }: OverlayMarkerProps) ); } -const StyledMarker = styled.div<{ isClicked: boolean }>` +const scaleUp = keyframes` + 0% { + transform: scale(1); + } + 100% { + transform: scale(1.5); + } +`; + +const StyledMarker = styled.div<{ isClicked: boolean; isRestaurantHovered: boolean }>` display: flex; justify-content: center; align-items: center; @@ -45,7 +58,8 @@ const StyledMarker = styled.div<{ isClicked: boolean }>` width: 36px; height: 36px; - border: ${({ isClicked }) => (isClicked ? '3px solid var(--orange-2)' : '3px solid transparent')}; + border: ${({ isClicked, isRestaurantHovered }) => + isClicked || isRestaurantHovered ? '3px solid var(--orange-2)' : '3px solid transparent'}; border-radius: 50%; transition: transform 0.2s ease-in-out; @@ -54,6 +68,12 @@ const StyledMarker = styled.div<{ isClicked: boolean }>` &:hover { transform: scale(1.5); } + + ${({ isRestaurantHovered }) => + isRestaurantHovered && + css` + animation: ${scaleUp} 0.2s ease-in-out forwards; + `} `; const fadeInAnimation = keyframes` diff --git a/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx b/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx index e5dbbe3a9..58f63f8a1 100644 --- a/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx +++ b/frontend/src/components/@common/ProfileImage/ProfileImage.stories.tsx @@ -14,6 +14,6 @@ export const Default: Story = { args: { name: '누군가', imageUrl: 'https://avatars.githubusercontent.com/u/51052049?v=4', - size: 64, + size: '64px', }, }; diff --git a/frontend/src/components/@common/ProfileImage/ProfileImage.tsx b/frontend/src/components/@common/ProfileImage/ProfileImage.tsx index 9c88399c6..e2efe8345 100644 --- a/frontend/src/components/@common/ProfileImage/ProfileImage.tsx +++ b/frontend/src/components/@common/ProfileImage/ProfileImage.tsx @@ -4,7 +4,7 @@ interface ProfileImageProps extends React.HTMLAttributes { name: string; imageUrl: string; border?: boolean; - size?: number; + size?: string; } function ProfileImage({ name = '셀럽', imageUrl, size, border = false, ...props }: ProfileImageProps) { @@ -13,9 +13,9 @@ function ProfileImage({ name = '셀럽', imageUrl, size, border = false, ...prop export default ProfileImage; -const StyledProfile = styled.img<{ size: number; border: boolean }>` - width: ${({ size }) => (size ? `${size}px` : '100%')}; - height: ${({ size }) => (size ? `${size}px` : 'auto')}; +const StyledProfile = styled.img<{ size: string; border: boolean }>` + width: ${({ size }) => size || 'auto'}; + height: ${({ size }) => size || 'auto'}; border-radius: 50%; background: none; diff --git a/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx b/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx index f326bcb71..53ecd90ed 100644 --- a/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx +++ b/frontend/src/components/CategoryNavbar/CategoryNavbar.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import styled from 'styled-components'; -import { RestaurantCategory } from '~/@types/restaurant.types'; import NavItem from '~/components/@common/NavButton/NavButton'; - import isEqual from '~/utils/compare'; +import type { RestaurantCategory } from '~/@types/restaurant.types'; + interface Category { label: RestaurantCategory; icon: React.ReactNode; diff --git a/frontend/src/components/CelebBanner/CelebBanner.tsx b/frontend/src/components/CelebBanner/CelebBanner.tsx index 077b14997..9a78383cf 100644 --- a/frontend/src/components/CelebBanner/CelebBanner.tsx +++ b/frontend/src/components/CelebBanner/CelebBanner.tsx @@ -27,7 +27,7 @@ function CelebBanner({ return ( - + {name} {youtubeChannelName} 구독자 {subscriberCount / 10_000}만명 ∙ 음식점 {restaurantCount}개 diff --git a/frontend/src/components/CelebDropDown/CelebDropDown.tsx b/frontend/src/components/CelebDropDown/CelebDropDown.tsx index a560a35d6..54201f29e 100644 --- a/frontend/src/components/CelebDropDown/CelebDropDown.tsx +++ b/frontend/src/components/CelebDropDown/CelebDropDown.tsx @@ -40,7 +40,7 @@ function CelebDropDown({ celebs, externalOnClick, isOpen = false }: DropDownProp {celebs.map(({ id, name, profileImageUrl }) => (
- + {name}
{isEqual(selected, name) && } diff --git a/frontend/src/components/MapModalContent/MapModalContent.tsx b/frontend/src/components/MapModalContent/MapModalContent.tsx index b85084d36..9350e71f9 100644 --- a/frontend/src/components/MapModalContent/MapModalContent.tsx +++ b/frontend/src/components/MapModalContent/MapModalContent.tsx @@ -2,6 +2,7 @@ import { styled } from 'styled-components'; import { RestaurantModalInfo } from '~/@types/restaurant.types'; import { BORDER_RADIUS, FONT_SIZE } from '~/styles/common'; import TextButton from '../@common/Button'; +import { BASE_URL } from '~/App'; interface MapModalContentProps { content: RestaurantModalInfo; @@ -17,10 +18,7 @@ function MapModalContent({ content }: MapModalContentProps) {
{roadAddress}
{phoneNumber}
- + >; } -function RestaurantCard({ restaurant, celebs, size, type = 'list', onClick }: RestaurantCardProps) { +function RestaurantCard({ + restaurant, + celebs, + size, + type = 'list', + onClick = () => {}, + setHoveredId = () => {}, +}: RestaurantCardProps) { const { images, name, roadAddress, category } = restaurant; + const onMouseEnter = () => { + setHoveredId(restaurant.id); + }; + + const onMouseLeave = () => { + setHoveredId(null); + }; + return ( - - + +
{category} diff --git a/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx b/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx index b98ad425b..8bf925577 100644 --- a/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx +++ b/frontend/src/components/RestaurantCardList/RestaurantCardList.tsx @@ -1,16 +1,18 @@ import { styled } from 'styled-components'; import { useEffect, useState } from 'react'; import RestaurantCard from '../RestaurantCard/RestaurantCard'; -import type { RestaurantData, RestaurantListData } from '~/@types/api.types'; import { FONT_SIZE } from '~/styles/common'; import RestaurantCardListSkeleton from './RestaurantCardListSkeleton'; +import type { RestaurantData, RestaurantListData } from '~/@types/api.types'; + interface RestaurantCardListProps { restaurantDataList: RestaurantListData | null; loading: boolean; + setHoveredId: React.Dispatch>; } -function RestaurantCardList({ restaurantDataList, loading }: RestaurantCardListProps) { +function RestaurantCardList({ restaurantDataList, loading, setHoveredId }: RestaurantCardListProps) { const [prevCardNumber, setPrevCardNumber] = useState(18); useEffect(() => { @@ -24,7 +26,7 @@ function RestaurantCardList({ restaurantDataList, loading }: RestaurantCardListP 음식점 수 {restaurantDataList.totalElementsCount} 개 {restaurantDataList.content?.map(({ celebs, ...restaurant }: RestaurantData) => ( - {}} /> + ))} diff --git a/frontend/src/components/VideoPreview/VideoPreview.tsx b/frontend/src/components/VideoPreview/VideoPreview.tsx index 78b42ca14..4b3c261ad 100644 --- a/frontend/src/components/VideoPreview/VideoPreview.tsx +++ b/frontend/src/components/VideoPreview/VideoPreview.tsx @@ -42,7 +42,7 @@ function VideoPreview({ )} - + {title}
{celebName}
diff --git a/frontend/src/hooks/useOnClickOutside.ts b/frontend/src/hooks/useOnClickOutside.ts new file mode 100644 index 000000000..f95470cc2 --- /dev/null +++ b/frontend/src/hooks/useOnClickOutside.ts @@ -0,0 +1,19 @@ +import { useEffect, RefObject } from 'react'; + +export default function useOnClickOutside( + ref: RefObject, + handler: (event?: Event | MouseEvent) => void, +) { + useEffect(() => { + function onClickHandler(event: Event | MouseEvent) { + if (!ref?.current || ref?.current.contains(event?.target as Node)) { + return; + } + handler(event); + } + window.addEventListener('click', onClickHandler); + return () => { + window.removeEventListener('click', onClickHandler); + }; + }, [ref, handler]); +} diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage.tsx index c55dba921..8d3a0823e 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage.tsx @@ -9,11 +9,12 @@ import RESTAURANT_CATEGORY from '~/constants/restaurantCategory'; import { CELEBS_OPTIONS } from '~/constants/celebs'; import useFetch from '~/hooks/useFetch'; import getQueryString from '~/utils/getQueryString'; +import RestaurantCardList from '~/components/RestaurantCardList'; + import type { Celeb } from '~/@types/celeb.types'; import type { RestaurantListData } from '~/@types/api.types'; import type { CoordinateBoundary } from '~/@types/map.types'; import type { RestaurantCategory } from '~/@types/restaurant.types'; -import RestaurantCardList from '~/components/RestaurantCardList'; function MainPage() { const [isMapExpanded, setIsMapExpanded] = useState(false); @@ -22,6 +23,7 @@ function MainPage() { const [boundary, setBoundary] = useState(); const [celebId, setCelebId] = useState(-1); const [restaurantCategory, setRestaurantCategory] = useState('전체'); + const [hoveredId, setHoveredId] = useState(null); const { handleFetch } = useFetch('restaurants'); const fetchRestaurants = useCallback( @@ -68,10 +70,16 @@ function MainPage() { - + - +