소스 :
https://github.dev/braverokmc79/smple-react-nodejs
리액트 -장소 선택(place picker)
https://braverokmc79.github.io/react-place-picker/

1.백엔드 Nodejs
app.js
import path , { dirname }  from 'path';
import fs from 'node:fs/promises';
import bodyParser from 'body-parser';
import express from 'express';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
app.use(express.static('./images'));
app.use(bodyParser.json());
// CORS
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*'); // allow all domains
  res.setHeader('Access-Control-Allow-Methods', 'GET, PUT');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  next();
});
app.get('/places', async (req, res) => {
//  const fileContent = await fs.readFile('./backend/data/places.json');
    const placesFilePath = path.resolve(__dirname, './data/places.json');
    const fileContent = await fs.readFile(placesFilePath);
  const placesData = JSON.parse(fileContent);
  res.status(200).json({ places: placesData });
});
app.get('/user-places', async (req, res) => {
//  const fileContent = await fs.readFile('./backend/data/user-places.json');
  const placesFilePath = path.resolve(__dirname, './data/user-places.json');
  const fileContent = await fs.readFile(placesFilePath);
  const places = JSON.parse(fileContent);
  res.status(200).json({ places });
});
app.put('/user-places', async (req, res) => {
  const places = req.body.places;
  console.log(" places  : ",places)
  const placesFilePath = path.resolve(__dirname, './data/user-places.json');
  await fs.writeFile(placesFilePath, JSON.stringify(places));
  res.status(200).json({ message: 'User places updated!' });
});
// 404
app.use((req, res, next) => {
  if (req.method === 'OPTIONS') {
    return next();
  }
  res.status(404).json({ message: '404 - Not Found' });
});
app.listen(3000);
2. 프론트 엔드 React
App.jsx
import { useRef, useState, useCallback, useEffect } from "react";
import Places from "./components/Places.jsx";
import Modal from "./components/Modal.jsx";
import DeleteConfirmation from "./components/DeleteConfirmation.jsx";
import logoImg from "./assets/logo.png";
import AvailablePlaces from "./components/AvailablePlaces.jsx";
import { fetchUserPlaces, updateUserPlaces } from "./http.js";
import Error from "./components/Error.jsx";
function App() {
  const selectedPlace = useRef();
  const [userPlaces, setUserPlaces] = useState([]);
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const [errorUpdatingPlaces, setErrorUpdatingPlaces] = useState(false)
  const [isFetching, setIsFetching] = useState(false);
  const [error, setError] =useState(false);
  // 방문하고 싶습니다 ... fetch visit  list
  useEffect(() => {
    async function fetchPlaces(){
      setIsFetching(true);
      try{      
        const places=await fetchUserPlaces(); 
        setUserPlaces(places);  
        setIsFetching(false);   
      }catch(error){
        console.log("에러  : ", error);
        setError({message: error.message || 'Failed to fetch user places.' });
        setIsFetching(false);
      }           
     
    }
    fetchPlaces();
  }, []);
  function handleStartRemovePlace(place) {
    setModalIsOpen(true);
    selectedPlace.current = place;
  }
  function handleStopRemovePlace() {
    setModalIsOpen(false);
  }
  async function handleSelectPlace(selectedPlace) {
    setUserPlaces((prevPickedPlaces) => {
      if (!prevPickedPlaces) {
        prevPickedPlaces = [];
      }
      if (prevPickedPlaces.some((place) => place.id === selectedPlace.id)) {
        return prevPickedPlaces;
      }
      return [selectedPlace, ...prevPickedPlaces];
    });
    try{
      await updateUserPlaces([selectedPlace, ...userPlaces]);      
    }catch(error){
      //에러시 기존 장소
      setUserPlaces(userPlaces);
      setErrorUpdatingPlaces({
        message:error.message || 'Failed to update places.'
      })
    }
    
  }
  const handleRemovePlace = useCallback(async function handleRemovePlace() {
    setUserPlaces((prevPickedPlaces) =>
      prevPickedPlaces.filter((place) => place.id !== selectedPlace.current.id)
    );
    try{
      await updateUserPlaces(
        userPlaces.filter((place) => place.id !== selectedPlace.current.id)
      )
    }catch(error){
        setUserPlaces(userPlaces);
        setErrorUpdatingPlaces({
          message:error.message || 'Failed to delete places.'
        })
    }
    setModalIsOpen(false);
  }, [userPlaces]);
  function handleError(){
    setErrorUpdatingPlaces(null);
  }
  return (
    <>
      <Modal open={errorUpdatingPlaces}   onClose={handleError} >
        <Error 
          title="에러 발생됨!"
          message={errorUpdatingPlaces.message}
          onConfirm={handleError}
        />
      </Modal>
      
      <Modal open={modalIsOpen} onClose={handleStopRemovePlace}>
        <DeleteConfirmation
          onCancel={handleStopRemovePlace}
          onConfirm={handleRemovePlace}
        />
      </Modal>
      <header>
        <img src={logoImg} alt="Stylized globe" />
        <h1>PlacePicker</h1>
        <p>
          Create your personal collection of places you would like to visit or
          you have visited.
        </p>
      </header>
      <main>
     
        {error && <Error  title='에러 발생!'   message={error.message}    /> }
        {!error&& <Places
          title="방문하고 싶습니다 ..."
          fallbackText="아래에서 방문하고 싶은 장소를 선택하세요."         
          isLoading={isFetching}
          loadingText="장소를 가져오는 중..."
          places={userPlaces}
          onSelectPlace={handleStartRemovePlace}
        />}
        <AvailablePlaces onSelectPlace={handleSelectPlace} />
      </main>
    </>
  );
}
export default App;
AvailablePlaces.jsx
import { useEffect, useState } from "react";
import Places from "./Places.jsx";
import Error from "./Error.jsx";
import { sortPlacesByDistance } from "../loc.js";
import { fetchAvailablePlaces } from "../http.js";
export default function AvailablePlaces({ onSelectPlace }) {
  const [isFetching, setIsFetching] = useState(false);
  const [availablePlaces, setAvailablePlaces] = useState([]);
  const [error, setError] =useState();
  useEffect(() => {
    async function fetchPlaces(){
      setIsFetching(true);
      try{
        
        const places=await fetchAvailablePlaces();
        navigator.geolocation.getCurrentPosition((position)=>{
          const sortedPlaces=sortPlacesByDistance(places, position.coords.latitude, position.coords.longitude);
          setAvailablePlaces(sortedPlaces);    
          setIsFetching(false);
        });
        
      }catch(error){
        console.log("에러  : ", error);
        setError({message: error.message || 'Could not fetch places, please try again later.' });
        setIsFetching(false);
      }           
     
    }
    fetchPlaces();
  }, []);
  if(error){
    return <Error title="에러 발생됨!" message={error.message}  />
  }
  return (
    <Places
      title="Available Places"
      places={availablePlaces}
      
      isLoading={isFetching}
      loadingText="데이터를 가져오는 중입니다...."
      fallbackText="No places available."
      onSelectPlace={onSelectPlace}
    />
  );
}
Places.jsx
export default function Places({ title, places, fallbackText, onSelectPlace, isLoading ,loadingText }) {
  return (
    <section className="places-category">
      <h2>{title}</h2>
      {isLoading && <p className="fallback-text">{loadingText}</p>}
      
      {!isLoading && places.length === 0 && <p className="fallback-text">{fallbackText}</p>}
      {!isLoading && places.length > 0 && (
        <ul className="places">
          {places.map((place) => (
            <li key={place.id} className="place-item">
              <button onClick={() => onSelectPlace(place)}>
                <img src={`http://localhost:3000/${place.image.src}`} alt={place.image.alt} />
                <h3>{place.title}</h3>
              </button>
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}
 
									













 
댓글 ( 0)  
댓글 남기기