React

 

 

 

소스 :

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>
  );
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

새도 가지를 가려서 앉는다 , 처신을 가려서 하라는 말.

댓글 ( 0)

댓글 남기기

작성