[React] 맛집 리스트 PJ - (4)

박하늘·2025년 4월 17일

Project

나만의 맛집 페이지를 구현하는 프로젝트

📍 4단계

추가 작업

  • 사용자 위치 및 거리순 정렬 컴포넌트 Context로 구현하기
  • 정렬( 거리순 / 기본순 ) 버튼 클릭 시 Favorite 목록도 같이 변경
  • Detail 페이지 구현 : 카드 클릭 시 해당 카드에 대한 DetailPage 화면 출력
  • CSS 마무리

프로젝트 구조

EATINGMARK-FE
├── src/
│   ├── components/
│   ├── pages/
│   ├── context/
│   ├── api/
│   ├── styles/
│   └── App.jsx
└── public/



1️⃣ 정렬( 거리순 / 기본순 ) 추가 기능 구현

  • 사용자 위치 및 거리순 정렬 컴포넌트 Context로 구현하기
  • 정렬( 거리순 / 기본순 ) 버튼 클릭 시 Favorite 목록도 같이 변경

📁 UserLocation.jsx

import { Children, createContext, useContext, useEffect, useState } from "react";
import { GetFetchApi } from "./GetFetchApi";
import { sortPlacesByDistance } from "../components/loc";
import { FavoriteFetchApi } from "./FavoriteFetchApi";

export const UserLocation = createContext();

export const UserLocationProvider = ({children}) => {

    const {places} = useContext(GetFetchApi)
    const {userPlaces} = useContext(FavoriteFetchApi)

    const [userLocation, setUserLocation] = useState(null)

    const [sortedPlaces, setSortedPlaces] = useState(null); // null로 초기화
    const [sortedFavPlaces, setSortedFavPlaces] = useState(null); // null로 초기화
    
    const [isSorted, setIsSorted] = useState(false); // 정렬 상태 확인용

    useEffect(() => {        
            navigator.geolocation.getCurrentPosition((position) => {
                setUserLocation({
                    lon: position.coords.longitude,
                    lat: position.coords.latitude
                })
            }, (err) => {
                console.error("위치 정보를 가져오는 데 실패했어요!", err)
              })
        },[])
    
    const sortedClick = () => {
        if(!isSorted){
            setSortedPlaces(userLocation ? sortPlacesByDistance(places, userLocation.lat, userLocation.lon)
                        : places)
            setSortedFavPlaces(userLocation ? sortPlacesByDistance(userPlaces, userLocation.lat, userLocation.lon)
                        : userPlaces)
        } else {
            setSortedPlaces(null);
            setSortedFavPlaces(null)
        }
        setIsSorted((prev) => !prev);
        }

    return(
        <UserLocation.Provider value={{sortedClick, sortedPlaces, isSorted, sortedFavPlaces}}>
            {children}
        </UserLocation.Provider>
    )
}
  • isSorted 를 이용하여 정렬 상태를 확인해주고 Main 컴포넌트에서 정렬 상태가 변경되면 Favorite 컴포넌트도 동일하게 변경되게끔 두 개 함수 모두 적용
  • sortedClick : 정렬 상태 확인 ⇢ 전체 목록 및 찜 목록 모두 정렬 할 수 있도록 구현

📁 Main.jsx

import { useContext} from "react"
import { Card } from "../components/Card"
import styles from "../styles/main.module.scss"
import { Favorite } from "./Favorite"
import { GetFetchApi } from "../context/GetFetchApi"
import { UserLocation } from "../context/UserLocation"

export function Main () {
    
    const { places } = useContext(GetFetchApi)
    const {sortedClick, sortedPlaces, isSorted} =useContext(UserLocation)

    return(
        <div className={styles.firdiv}>
            <Favorite/>
            <div className={styles.secdiv}>
                <h1 className={styles.h1}> All Place ... </h1>
                <button className={isSorted ? styles.button : styles.button1}
                onClick={() => sortedClick()}> {isSorted ? "기본순" : "거리순"}</button>
            </div>
            <div className={styles.thrdiv}>
                {sortedPlaces ? 
                sortedPlaces.map((place) => <Card key={place.id} place={place}/>) 
                : places.map((place) => <Card key={place.id} place={place}/>)}

            </div>
        </div>
    )
}

📁 Favorite.jsx

import { useContext } from "react"
import { FavoriteFetchApi } from "../context/FavoriteFetchApi"
import styles from "../styles/favorite.module.scss"
import { Card } from "../components/Card" 
import { UserLocation } from "../context/UserLocation"

export function Favorite () {

    const {userPlaces} = useContext(FavoriteFetchApi)
    const {sortedFavPlaces} = useContext(UserLocation)

    const displaySorted = sortedFavPlaces || userPlaces

    return(
        <>
        <div className={styles.container}>
        <div className={styles.leftSide}>
            <h1 className={styles.h1}>Favorite</h1>
            <div className={styles.sidediv}></div>
        </div>

        <div className={styles.secdiv}>
            {displaySorted.length > 0 ? (
            displaySorted.map((place) => <Card key={place.id} place={place} />)
            ) : (
            <p>찜한 맛집이 없습니다.</p>
            )}
        </div>
        </div>
        </>
    )
}



2️⃣ Detail 페이지 구현

  • URL 주소 ( <Route path={"/detail/:placeId"} element={<Detail />}/> ) 중 placeId 값을 useParams 로 받아와서 해당 Id 인 값 화면에 출력

📁 Detail.jsx

import { useParams } from "react-router"
import { Card } from "../components/Card"
import { GetFetchApi } from "../context/GetFetchApi"
import { useContext } from "react"
import styles from "../styles/detail.module.scss"

export function Detail () {
    const baseURL = import.meta.env.VITE_BASE_URL

    const { places } = useContext(GetFetchApi)

    const { placeId } = useParams()

    const selectedPlace = places.find((el) => String(el.id) === String(placeId));

    // console.log(selectedPlace)

    return(
        <>
            <div className={styles.firdiv}>
                <h1>{selectedPlace.title}</h1>
                <img src={`${baseURL}/${selectedPlace.image.src}`}/>
                <div></div>
                <p> {selectedPlace.description} </p>
            </div>
        </>
    )
}
  • 이 Detail 페이지는 Card 컴포넌트에 연결

📁 Card.jsx

  • useNavigate 로 연결하여 화면에 출력될 수 있도록 구현
import { useNavigate } from "react-router"
import styles from "../styles/card.module.scss"
import FavoriteBtn from "./FavoriteBtn";


export const Card = ({place}) => {
    const navigate = useNavigate()
    const BASE_URL = import.meta.env.VITE_BASE_URL;


    
    return(
        <>
        <section className={styles.section}
        onClick={() => navigate(`/detail/${place.id}`)}>
            <img src={`${BASE_URL}/${place.image.src}`} alt={place.title}/>
            <div> {place.title} </div>
            <FavoriteBtn place={place} />
        </section>
        </>
    )
}




3️⃣ CSS 수정

0개의 댓글