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