소스 :
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)
댓글 남기기