Select Git revision
Home.js 15.45 KiB
import React, { useEffect, useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import AddPlace from '../components/AddPlace';
import PlaceDirections from '../components/PlaceDirections';
import { FaArrowLeft, FaUser } from 'react-icons/fa';
import MyTrip from '../components/MyTrip';
import Map from '../components/Map';
import PlaceDetails from '../components/PlaceDetails';
import NewTrip from '../components/NewTrip';
import useKakaoMap from '../hooks/useKakaoMap';
import axios from 'axios';
import '../styles/HomePage.css';
import MapSearch from '../components/MapSearch';
import { toast } from 'react-toastify';
const API_URL = process.env.REACT_APP_SERVER_URL;
const WS_URL = process.env.REACT_APP_WS_URL;
const Home = () => {
const navigate = useNavigate();
const [activeComponent, setActiveComponent] = useState('myTrip');
const [activeTab, setActiveTab] = useState('attractions');
const [selectedPlace, setSelectedPlace] = useState(null);
const [selectedTrip, setselectedTrip] = useState(null);
const [showNewTrip, setShowNewTrip] = useState(false);
const [trips, setTrips] = useState([]);
const [showDirection, setShowDirection] = useState(false);
const [directionInfo, setDirectionInfo] = useState(null);
const [selectedLocation, setSelectedLocation] = useState({
latitude: 35.1586,
longitude: 129.1603
});
const [ws, setWs] = useState(null);
const { map, setMap, markers, setMarkers, clearMap } = useKakaoMap(selectedLocation);
useEffect(() => {
const fetchTrips = async () => {
try {
const token = localStorage.getItem('token');
const response = await axios.get(
`${API_URL}/api/trips/trips`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
setTrips(response.data);
} catch (error) {
if (error.response?.status === 401) {
toast.error('로그인이 필요합니다');
navigate('/login');
} else {
toast.error('여행 데이터를 불러오는데 실패했습니다');
console.error('여행 데이터를 불러오는데 실패했습니다:', error);
}
}
};
fetchTrips();
}, []);
useEffect(() => {
if (ws && ws.readyState === WebSocket.OPEN && trips.length > 0) {
ws.send(JSON.stringify({
type: 'tripsUpdate',
data: trips
}));
}
}, [trips, ws]);
useEffect(() => {
const websocket = new WebSocket(WS_URL);
websocket.onopen = () => {
console.log('WebSocket 연결됨');
};
websocket.onmessage = (event) => {
const change = JSON.parse(event.data);
switch (change.type) {
case 'tripsUpdate':
// 다른 클라이언트에서 trips가 업데이트된 경우
setTrips(change.data);
break;
case 'update':
// 여행 정보가 업데이트된 경우
refreshTrips();
toast.success('여행 정보가 업데이트되었습니다.');
break;
case 'delete':
// 여행이 삭제된 경우
setTrips(prev => prev.filter(trip => trip._id !== change.documentId));
toast.info('여행이 삭제되었습니다.');
break;
case 'addPlace':
// 새로운 장소가 추가된 경우
refreshTrips();
toast.success('새로운 장소가 추가되었습니다.');
break;
case 'removePlace':
// 장소가 제거된 경우
refreshTrips();
toast.info('장소가 제거되었습니다.');
break;
case 'create':
// 새로운 여행이 생성된 경우
refreshTrips();
toast.success('새로운 여행이 생성되었습니다.');
break;
}
};
websocket.onerror = (error) => {
console.error('WebSocket 에러:', error);
toast.error('실시간 업데이트 연결에 실패했습니다.');
};
setWs(websocket);
return () => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.close();
}
};
}, []);
const toggleNewTrip = () => {
setShowNewTrip(!showNewTrip);
setSelectedPlace(null);
setShowDirection(false);
setTimeout(() => {
map?.relayout();
}, 200);
};
const handlePlaceSelect = useCallback((place) => {
if (!map) return;
console.log(place);
clearMap();
setSelectedPlace(place);
const position = new window.kakao.maps.LatLng(
place.coordinates.lat,
place.coordinates.lng
);
const marker = new window.kakao.maps.Marker({
position: position,
map
});
setMarkers([{ marker }]);
setSelectedLocation({
latitude: place.coordinates.lat,
longitude: place.coordinates.lng
});
map.setCenter(position);
map.setLevel(4);
setTimeout(() => {
map?.relayout();
}, 200);
}, [map, clearMap]);
const createCustomOverlay = useCallback((position, content) => {
const element = document.createElement('div');
element.className = 'marker-label';
element.innerHTML = content;
return new window.kakao.maps.CustomOverlay({
position,
content: element,
yAnchor: 0,
map
});
}, [map]);
const handleTripSelect = useCallback((trip) => {
if (!map) return;
setselectedTrip(trip);
clearMap();
const newMarkers = [];
const bounds = new window.kakao.maps.LatLngBounds();
const linePath = [];
const allPlaces = Object.values(trip.plans)
.flatMap(day => day.places);
if (allPlaces.length === 0) {
const geocoder = new window.kakao.maps.services.Geocoder();
geocoder.addressSearch(trip.location, (result, status) => {
if (status === window.kakao.maps.services.Status.OK) {
const position = new window.kakao.maps.LatLng(
result[0].y,
result[0].x
);
map.setCenter(position);
map.setLevel(8);
map.relayout();
} else {
console.error('주소를 좌표로 변환하는데 실패했습니다.');
}
});
return;
}
const markerClusterer = new window.kakao.maps.MarkerClusterer({
map: map,
averageCenter: true,
minLevel: 5,
disableClickZoom: true,
styles: [{
width: '50px',
height: '50px',
background: 'rgba(255, 69, 0, 0.8)',
borderRadius: '25px',
color: '#fff',
textAlign: 'center',
lineHeight: '50px',
fontSize: '14px',
fontWeight: 'bold'
}]
});
const overlayClusterer = new window.kakao.maps.MarkerClusterer({
map: map,
averageCenter: true,
minLevel: 5,
disableClickZoom: true,
styles: [{
width: '50px',
height: '50px',
background: 'rgba(0, 0, 0, 0.6)',
borderRadius: '25px',
color: '#fff',
textAlign: 'center',
lineHeight: '50px',
fontSize: '14px',
fontWeight: 'bold'
}]
});
const markers = allPlaces.map((place, index) => {
const position = new window.kakao.maps.LatLng(
place.coordinates.lat,
place.coordinates.lng
);
const marker = new window.kakao.maps.Marker({ position });
const overlayMarker = new window.kakao.maps.Marker({
position,
opacity: 0
});
const overlay = createCustomOverlay(position, `${index + 1}. ${place.name}`);
window.kakao.maps.event.addListener(marker, 'click', () => {
handlePlaceSelect(place);
});
bounds.extend(position);
linePath.push(position);
newMarkers.push({ marker, overlay });
return {
marker,
overlayMarker
};
});
markerClusterer.addMarkers(markers.map(m => m.marker));
overlayClusterer.addMarkers(markers.map(m => m.overlayMarker));
const polyline = new window.kakao.maps.Polyline({
path: linePath,
strokeWeight: 3,
strokeColor: '#FF4500',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
map.polyline = polyline;
map.relayout();
map.setBounds(bounds);
window.kakao.maps.event.addListener(markerClusterer, 'clusterclick', (cluster) => {
const level = map.getLevel() - 1;
map.setLevel(level, { anchor: cluster.getCenter() });
});
setMarkers(newMarkers);
}, [map, clearMap, handlePlaceSelect, createCustomOverlay]);
const handleRouteChange = async (tripId, day, newRoute, places, newName) => {
try {
if (newName) {
// 여행 이름 수정
const response = await axios.put(
`${API_URL}/api/trips/trips/${tripId}`,
{ name: newName }
);
if (response.status === 200) {
refreshTrips();
}
} else if (day && newRoute) {
// 기존 경로 수정 로직
const requestData = {
dayKey: day,
places: places,
route: newRoute
};
const response = await axios.put(
`${API_URL}/api/trips/trips/${tripId}/plans/day`,
requestData
);
if (response.status === 200) {
toast.success('경로가 성공적으로 업데이트되었습니다.');
refreshTrips();
}
}
} catch (error) {
console.error('업데이트 실패:', error);
toast.error('업데이트에 실패했습니다.');
}
};
const handleDirectionSelect = (startPlace, endPlace) => {
setShowDirection(true);
setDirectionInfo({ startPlace, endPlace });
setSelectedPlace(null);
};
const handleAddPlace = (tripId, day) => {
const selectedTrip = trips.find(trip => trip._id === tripId);
if (!selectedTrip) {
console.error('선택된 여행을 찾을 수 없습니다.');
return;
}
setselectedTrip({
tripId,
day,
location: selectedTrip.location
});
setActiveComponent('addPlace');
};
const handleBack = () => {
setActiveComponent('myTrip');
setselectedTrip(null);
setSelectedPlace(null);
};
const handleSearchResult = useCallback((searchResults) => {
if (!map) return;
clearMap();
const newMarkers = [];
const bounds = new window.kakao.maps.LatLngBounds();
searchResults.forEach(place => {
const position = new window.kakao.maps.LatLng(
place.coordinates.lat,
place.coordinates.lng
);
const marker = new window.kakao.maps.Marker({
position: position,
map: map
});
const overlay = createCustomOverlay(position, place.name);
window.kakao.maps.event.addListener(marker, 'click', () => {
const placeData = {
id: place.id,
name: place.name,
location: place.address,
coordinates: place.coordinates,
tel: place.phone
};
handlePlaceSelect(placeData);
});
bounds.extend(position);
newMarkers.push({ marker, overlay });
});
setMarkers(newMarkers);
map.setBounds(bounds);
map.relayout();
}, [map, clearMap, createCustomOverlay, handlePlaceSelect]);
const handleTripDelete = async (tripId) => {
try {
await axios.delete(`${API_URL}/api/trips/trips/${tripId}`);
setTrips(prevTrips => prevTrips.filter(trip => trip._id !== tripId));
toast.success('여행이 성공적으로 삭제되었습니다.');
} catch (error) {
console.error('여행 삭제 중 오류 발생:', error);
toast.error('여행 삭제 중 오류가 발생했습니다.');
}
};
const handleTripCreate = (newTrip) => {
setTrips(prevTrips => [...prevTrips, newTrip]);
};
const refreshTrips = async () => {
try {
const token = localStorage.getItem('token');
const response = await axios.get(
`${API_URL}/api/trips/trips`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
setTrips(response.data);
} catch (error) {
if (error.response?.status === 401) {
toast.error('로그인이 필요합니다');
navigate('/login');
} else {
toast.error('여행 데이터를 불러오는데 실패했습니다');
console.error('여행 데이터를 불러오는데 실패했습니다:', error);
}
}
};
return (
<div className="container">
<div className={`wrapper ${showNewTrip ? 'full-content' : ''} ${(showDirection || selectedPlace) ? 'show-content' : ''}`}>
<div className="sidebar">
<div className="header-container">
{activeComponent === 'addPlace' && (
<button className="back-button" onClick={handleBack}>
<FaArrowLeft />
</button>
)}
<h1 className="festivelo-logo">FESTIVELO</h1>
<button
className="profile-button"
onClick={() => navigate('/my')}
>
<FaUser />
</button>
</div>
{activeComponent === 'addPlace' ? (
<AddPlace
tripId={selectedTrip?.tripId}
day={selectedTrip?.day}
onBack={handleBack}
activeTab={activeTab}
onTabChange={setActiveTab}
onPlaceSelect={handlePlaceSelect}
tripStartDate={trips.find(trip => trip._id === selectedTrip?.tripId)?.start_date}
tripEndDate={trips.find(trip => trip._id === selectedTrip?.tripId)?.end_date}
location={trips.find(trip => trip._id === selectedTrip?.tripId)?.location}
onSuccess={refreshTrips}
/>
) : (
<>
<div className="title-container">
<h2 className="title">내 여행</h2>
<button
className="add-trip-button"
onClick={toggleNewTrip}
>
여행 추가
</button>
</div>
<MyTrip
trips={trips}
onPlaceSelect={handlePlaceSelect}
onAddPlace={handleAddPlace}
onDirectionSelect={handleDirectionSelect}
onTripSelect={handleTripSelect}
onRouteChange={handleRouteChange}
onTripDelete={handleTripDelete}
/>
</>
)}
</div>
<div className="content">
{showNewTrip ? (
<NewTrip
onClose={toggleNewTrip}
onTripCreate={handleTripCreate}
/>
) : showDirection ? (
<PlaceDirections
startPlace={directionInfo.startPlace}
endPlace={directionInfo.endPlace}
map={map}
onClose={() => setShowDirection(false)}
/>
) : selectedPlace && (
<PlaceDetails
place={selectedPlace}
onClose={() => setSelectedPlace(null)}
/>
)}
</div>
<div className={`map-container ${showNewTrip ? 'hidden' : ''}`}>
{!showNewTrip && !showDirection && !selectedPlace && (
<MapSearch
map={map}
onSearchResult={handleSearchResult}
clearMap={clearMap}
setMarkers={setMarkers}
trips={trips}
/>
)}
<Map
latitude={selectedLocation.latitude}
longitude={selectedLocation.longitude}
markers={markers}
map={map}
setMap={setMap}
/>
</div>
</div>
</div>
);
};
export default Home;