diff --git a/.github/workflows/front-build.yaml b/.github/workflows/front-build.yaml index d54b06a..17c6acd 100644 --- a/.github/workflows/front-build.yaml +++ b/.github/workflows/front-build.yaml @@ -34,6 +34,11 @@ jobs: - name: install npm dependencies run: npm install + - name: Create .env file + run: | + echo "VITE_NAVER_MAP_ID=${{ secrets.VITE_NAVER_MAP_ID }}" >> .env + echo "VITE_SERVER_ADDRESS=${{ secrets.VITE_SERVER_ADDRESS }}" >> .env + - name: react build run: npm run build diff --git a/src/assets/a.webp b/src/assets/a.webp new file mode 100644 index 0000000..14153b5 Binary files /dev/null and b/src/assets/a.webp differ diff --git a/src/assets/b.webp b/src/assets/b.webp new file mode 100644 index 0000000..eb8b6cf Binary files /dev/null and b/src/assets/b.webp differ diff --git a/src/assets/c.webp b/src/assets/c.webp new file mode 100644 index 0000000..d67d49a Binary files /dev/null and b/src/assets/c.webp differ diff --git a/src/assets/d.webp b/src/assets/d.webp new file mode 100644 index 0000000..8240da5 Binary files /dev/null and b/src/assets/d.webp differ diff --git a/src/assets/f.webp b/src/assets/f.webp new file mode 100644 index 0000000..883217c Binary files /dev/null and b/src/assets/f.webp differ diff --git a/src/pages/MapPage/BottomSheet.tsx b/src/pages/MapPage/BottomSheet.tsx index a749bc2..984a08a 100644 --- a/src/pages/MapPage/BottomSheet.tsx +++ b/src/pages/MapPage/BottomSheet.tsx @@ -5,6 +5,8 @@ import { BottomSheetProps } from './interface'; import ListContainer from './components/ListContainer'; import CommentList from './components/comment/CommentList'; import NoInfo from './components/NoInfo'; +import SearchResult from './components/SearchResult'; +import { Slope } from '../../apis/slopeMap'; const BottomSheet: React.FC = ({ slopeData, @@ -75,6 +77,40 @@ const BottomSheet: React.FC = ({ scrollWrapperRef.current.style.overflow = 'auto'; } }; + + const countGrades = (slopes: Slope[]) => { + const counts = { + aCount: 0, + bCount: 0, + cCount: 0, + dCount: 0, + fCount: 0, + }; + + for (let i = 0; i < slopes.length; i++) { + const grade = slopes[i].priority.grade.toUpperCase(); + + switch (grade) { + case 'A': + counts.aCount++; + break; + case 'B': + counts.bCount++; + break; + case 'C': + counts.cCount++; + break; + case 'D': + counts.dCount++; + break; + case 'F': + counts.fCount++; + break; + } + } + return counts; + }; + return ( @@ -103,22 +139,36 @@ const BottomSheet: React.FC = ({ return ; } } else { + const { aCount, bCount, cCount, dCount, fCount } = + countGrades(slopeData); return ( - - {!slopeData || slopeData.length === 0 ? ( -
데이터 조회 중
- ) : ( - slopeData.map((item, index) => ( - { - onItemClick(item, index); - }} - > - )) - )} -
+ <> + + + {!slopeData || slopeData.length === 0 ? ( +
데이터 조회 중
+ ) : ( + slopeData.map((item, index) => ( + { + onItemClick(item, index); + }} + > + )) + )} +
+ ); } })()} diff --git a/src/pages/MapPage/MapPage.tsx b/src/pages/MapPage/MapPage.tsx index f48fa4b..2519f4b 100644 --- a/src/pages/MapPage/MapPage.tsx +++ b/src/pages/MapPage/MapPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import styled from 'styled-components'; @@ -6,144 +6,69 @@ import BottomSheet from './BottomSheet'; import MapComponent from './components/map/MapComponent'; import SearchComponent from './components/map/Search'; -import { Slope, slopeMapAPI } from '../../apis/slopeMap'; -import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; +import { useMapStore } from './mapStore'; +import ButtonGroup from './components/ButtonGroup'; const MapPage = () => { - // console.log(escarpmentData); - // console.log(escarpmentData); - const [selectedMarkerId, setSelectedMarkerId] = useState(null); - const [allTextShow, setAllTextShow] = useState(false); - const [userLocation, setUserLocation] = useState( - null - ); - const [slopeData, setSlopeData] = useState([]); - - const [searchMod, setSearchMod] = useState(false); - const [bottomSheetHeight, setBottomSheetHeight] = useState(200); //bottomsheet 높이 조절 state - - const fetchSlopes = useCallback(async () => { - //위치정보가 없는 경우 호출 안함 - if (!userLocation?.lat() || !userLocation?.lng()) return; - - try { - const data = await slopeMapAPI.fetchNearbySlopes( - userLocation.lat(), - userLocation.lng() - ); - setSlopeData(data || []); - } catch (error) { - console.error('Error fetching slopes:', error); - setSlopeData([]); - } - }, [userLocation]); + const { + selectedMarkerId, + allTextShow, + userLocation, + slopeData, + searchMod, + bottomSheetHeight, + mapInstance, + setBottomSheetHeight, + fetchSlopes, + handleSearch, + chooseSelectItem, + setUserLocation, + setMapInstance, + setSelectedMarkerId, + } = useMapStore(); useEffect(() => { if (!searchMod) fetchSlopes(); }, [userLocation, searchMod, fetchSlopes]); - const [mapInstance, setMapInstance] = useState(null); - - //검색 핸들 callback - const handleSearch = useCallback( - (searchValue: string) => { - if (searchValue === '') { - setSearchMod(false); - fetchSlopes(); - setSelectedMarkerId(null); - return; - } - setSelectedMarkerId(null); - setSearchMod(true); - - const searchSlope = async () => { - if (!userLocation?.lat() || !userLocation?.lng()) return; //위치정보가 없는 경우 호출 안함 - // console.log('Searching for:', searchValue); - // console.log('Searching Mod:', searchMod); - try { - const data = await slopeMapAPI.searchSlopes( - searchValue, - userLocation.lat(), - userLocation.lng() - ); - setSlopeData(data || []); - if (mapInstance && data) { - const coordinates = data[0].location.coordinates.start.coordinates; - mapInstance.panTo( - new naver.maps.LatLng(coordinates[1], coordinates[0]) - ); - } - } catch (error) { - console.error('Error search slopes:', error); - setSlopeData([]); - } - }; - searchSlope(); - }, - [userLocation] - ); - - //아이템 선택 - const chooseSelectItem = useCallback( - (item: Slope, index: number) => { - if (mapInstance && item) { - // 지도 이동 - const coordinates = item.location.coordinates.start.coordinates; - mapInstance.panTo( - new naver.maps.LatLng(coordinates[1], coordinates[0]) - ); - - // 마커 선택 상태 변경 - setSelectedMarkerId((prevId) => (prevId === index ? null : index)); - } - }, - [mapInstance] - ); - - //내 위치로 이동 - const moveToMyLocation = useCallback(() => { - if (!mapInstance || !userLocation) return; - - // 줌 레벨 먼저 변경 - mapInstance.setZoom(15); - // 약간 지연 후 위치 이동 - setTimeout(() => { - mapInstance.panTo(userLocation); - }, 100); - - fetchSlopes(); - }, [mapInstance, userLocation, fetchSlopes]); return ( - - - { - setSelectedMarkerId(null); + <> + + + { + setSelectedMarkerId(null); + }} + searchMod={searchMod} + /> + + + {/* { + setAllTextShow(!allTextShow); }} - searchMod={searchMod} - /> - - + > + {allTextShow ? '위성지도' : '일반지도'} + { - // console.log(allTextShow); - // console.log(allTextShow); setAllTextShow(!allTextShow); }} > @@ -151,8 +76,10 @@ const MapPage = () => { - - + */} + + + ); }; @@ -166,47 +93,3 @@ const BaseBackground = styled.div` overscroll-behavior: none; position: relative; `; - -const AllShowButton = styled.button<{ $isSelect: boolean }>` - position: absolute; - top: 50px; - right: 10px; - border: none; - border-radius: 8px; - height: 30px; - padding: 5px 10px; - box-shadow: ${({ theme }) => theme.shadows.sm}; - font-weight: ${({ theme }) => theme.fonts.weights.bold}; - font-size: ${({ theme }) => theme.fonts.sizes.ms}; - background-color: ${({ $isSelect, theme }) => - $isSelect ? theme.colors.primaryDark : '#fff'}; - color: ${({ $isSelect, theme }) => - !$isSelect ? theme.colors.primaryDark : '#fff'}; - &:focus { - outline: none; - } - transition: all 0.15s ease-in-out; - - &:active { - transform: scale(1.1); - } -`; - -const MyPosition = styled.button` - position: absolute; - top: 90px; - right: 10px; - border: none; - border-radius: 8px; - padding: 5px 10px; - box-shadow: ${({ theme }) => theme.shadows.sm}; - font-weight: 550; - background-color: #fff; - transition: all 0.15s ease-in-out; - &:hover { - background-color: ${({ theme }) => theme.colors.grey[200]}; - } - &:active { - transform: scale(1.1); - } -`; diff --git a/src/pages/MapPage/components/ButtonGroup.tsx b/src/pages/MapPage/components/ButtonGroup.tsx new file mode 100644 index 0000000..f061b9b --- /dev/null +++ b/src/pages/MapPage/components/ButtonGroup.tsx @@ -0,0 +1,185 @@ +// import styled from 'styled-components' +// import { useMapStore } from '../mapStore'; +// import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; + +// const ButtonGroup = () => { +// const {allTextShow,setAllTextShow,moveToMyLocation}=useMapStore(); +// return ( +// +// { +// setAllTextShow(!allTextShow); +// }} +// > +// {allTextShow ? '위성지도' : '일반지도'} +// +// { +// setAllTextShow(!allTextShow); +// }} +// > +// {allTextShow ? '전체표기' : '개별표기'} +// +// +// +// +// +// ); +// }; + +// export default ButtonGroup; + +// const Container=styled.div` +// position: absolute; +// top: 50px; +// right: 10px; +// ` +// const AllShowButton = styled.button<{ $isSelect: boolean }>` +// position: absolute; +// top: 50px; +// right: 10px; +// border: none; +// border-radius: 8px; +// height: 30px; +// padding: 5px 10px; +// box-shadow: ${({ theme }) => theme.shadows.sm}; +// font-weight: ${({ theme }) => theme.fonts.weights.bold}; +// font-size: ${({ theme }) => theme.fonts.sizes.ms}; +// background-color: ${({ $isSelect, theme }) => +// $isSelect ? theme.colors.primaryDark : '#fff'}; +// color: ${({ $isSelect, theme }) => +// !$isSelect ? theme.colors.primaryDark : '#fff'}; +// &:focus { +// outline: none; +// } +// transition: all 0.15s ease-in-out; + +// &:active { +// transform: scale(1.1); +// } +// `; + +// const MyPosition = styled.button` +// position: absolute; +// top: 90px; +// right: 10px; +// border: none; +// border-radius: 8px; +// padding: 5px 10px; +// box-shadow: ${({ theme }) => theme.shadows.sm}; +// font-weight: 550; +// background-color: #fff; +// transition: all 0.15s ease-in-out; +// &:hover { +// background-color: ${({ theme }) => theme.colors.grey[200]}; +// } +// &:active { +// transform: scale(1.1); +// } +// `; +import styled from 'styled-components'; +import { useMapStore, MapTypeId } from '../mapStore'; +import MyLocationIcon from '@mui/icons-material/MyLocationRounded'; + +const ButtonGroup = () => { + const { + allTextShow, + setAllTextShow, + moveToMyLocation, + mapTypeId, + setMapTypeId, + } = useMapStore(); + + return ( + + { + setMapTypeId( + mapTypeId === MapTypeId.NORMAL ? MapTypeId.HYBRID : MapTypeId.NORMAL + ); + }} + > + {mapTypeId === MapTypeId.HYBRID ? '일반지도' : '위성지도'} + + + { + setAllTextShow(!allTextShow); + }} + > + {allTextShow ? '전체표기' : '개별표기'} + + + + + + + ); +}; + +export default ButtonGroup; + +const Container = styled.div` + position: absolute; + top: 50px; + right: 10px; + display: flex; + flex-direction: column; + gap: 10px; + z-index: 1000; +`; + +// 버튼 기본 스타일을 공통으로 분리 +const BaseButton = styled.button` + border: none; + border-radius: 8px; + padding: 5px 10px; + box-shadow: ${({ theme }) => theme.shadows.sm}; + transition: all 0.15s ease-in-out; + + &:focus { + outline: none; + } + + &:active { + transform: scale(1.1); + } +`; + +// 지도 타입 변경 버튼 +const MapTypeButton = styled(BaseButton)<{ $isSelect: boolean }>` + height: 30px; + font-weight: ${({ theme }) => theme.fonts.weights.bold}; + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + background-color: ${({ $isSelect, theme }) => + $isSelect ? theme.colors.primaryDark : '#fff'}; + color: ${({ $isSelect, theme }) => + !$isSelect ? theme.colors.primaryDark : '#fff'}; +`; + +// 텍스트 표시 방식 변경 버튼 +const TextDisplayButton = styled(BaseButton)<{ $isSelect: boolean }>` + height: 30px; + font-weight: ${({ theme }) => theme.fonts.weights.bold}; + font-size: ${({ theme }) => theme.fonts.sizes.ms}; + background-color: ${({ $isSelect, theme }) => + $isSelect ? theme.colors.primaryDark : '#fff'}; + color: ${({ $isSelect, theme }) => + !$isSelect ? theme.colors.primaryDark : '#fff'}; +`; + +// 내 위치로 이동 버튼 +const LocationButton = styled(BaseButton)` + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + + &:hover { + background-color: ${({ theme }) => theme.colors.grey[200]}; + } +`; diff --git a/src/pages/MapPage/components/InfoTable.tsx b/src/pages/MapPage/components/InfoTable.tsx index a19b23c..02ae269 100644 --- a/src/pages/MapPage/components/InfoTable.tsx +++ b/src/pages/MapPage/components/InfoTable.tsx @@ -3,13 +3,13 @@ import { InfotableProps } from '../interface'; const InfoTable: React.FC = ({ selectItem, onCloseInfo }) => { if (!selectItem) return null; - const grade = selectItem.disaster?.riskLevel?.includes('A') + const grade = selectItem.priority?.grade?.includes('A') ? 'A' - : selectItem.disaster?.riskLevel?.includes('B') + : selectItem.priority?.grade?.includes('B') ? 'B' - : selectItem.disaster?.riskLevel?.includes('C') + : selectItem.priority?.grade?.includes('C') ? 'C' - : selectItem.disaster?.riskLevel?.includes('D') + : selectItem.priority?.grade?.includes('D') ? 'D' : 'F'; return ( @@ -35,22 +35,38 @@ const InfoTable: React.FC = ({ selectItem, onCloseInfo }) => { {selectItem?.management?.department || ''} - - {selectItem?.location?.province || ''}{' '} + {selectItem?.location?.province || ''} {selectItem?.location?.city || ''} {selectItem?.location?.district || ''} {selectItem?.location?.address || ''} + + {selectItem?.priority?.usage && ( + + + {selectItem.priority.usage} + + )} + + + {selectItem.priority.slopeNature} + + + + {selectItem.priority.slopeType} + {grade} + + - + {selectItem?.location?.coordinates?.start?.coordinates?.[1] && selectItem?.location?.coordinates?.start?.coordinates?.[0] @@ -63,6 +79,20 @@ const InfoTable: React.FC = ({ selectItem, onCloseInfo }) => { : '좌표 정보 없음'} + + + + {selectItem?.location?.coordinates?.end?.coordinates?.[1] && + selectItem?.location?.coordinates?.end?.coordinates?.[0] + ? `위도: ${selectItem.location.coordinates.end.coordinates[1] + .toFixed(6) + .toString()}° + 경도: ${selectItem.location.coordinates.end.coordinates[0] + .toFixed(6) + .toString()}°` + : '좌표 정보 없음'} + + ); @@ -134,16 +164,18 @@ const AddressValue = styled(Value)` `; const GradeValue = styled(Value)<{ $grade: string }>` - color: ${({ $grade }) => { + color: ${({ $grade, theme }) => { switch ($grade) { case 'A': - return '#2ecc71'; + return theme.colors.grade.A; case 'B': - return '#f1c40f'; + return theme.colors.grade.B; case 'C': - return '#e67e22'; + return theme.colors.grade.C; case 'D': - return '#e74c3c'; + return theme.colors.grade.D; + case 'F': + return theme.colors.grade.F; default: return '#333'; } @@ -162,3 +194,7 @@ const CloseButton = styled.button` } padding: 8px 15px; `; + +const Line = styled.div` + border-bottom: 1px dashed ${({ theme }) => theme.colors.grey[200]}; +`; diff --git a/src/pages/MapPage/components/ListContainer.tsx b/src/pages/MapPage/components/ListContainer.tsx index 380b0a0..12c5b7d 100644 --- a/src/pages/MapPage/components/ListContainer.tsx +++ b/src/pages/MapPage/components/ListContainer.tsx @@ -4,20 +4,20 @@ import { ListProps } from '../interface'; const ListContainer: React.FC = ({ item, onClick }) => { if (!item) return null; - const grade = item.disaster?.riskLevel.includes('A') + const grade = item.priority?.grade.includes('A') ? 'A' - : item.disaster?.riskLevel.includes('B') + : item.priority?.grade.includes('B') ? 'B' - : item.disaster?.riskLevel.includes('C') + : item.priority?.grade.includes('C') ? 'C' - : item.disaster?.riskLevel.includes('D') + : item.priority?.grade.includes('D') ? 'D' : 'F'; return ( - {grade} + {grade} @@ -69,38 +69,27 @@ const GradeBackground = styled.div<{ $grade: string }>` justify-content: center; align-items: center; flex-shrink: 0; - background-color: ${({ $grade }) => { + background-color: ${({ $grade, theme }) => { switch ($grade) { case 'A': - return 'rgba(46, 204, 113, 0.15)'; // #2ecc71 with opacity + return theme.colors.grade.A; case 'B': - return 'rgba(241, 196, 15, 0.15)'; // #f1c40f with opacity + return theme.colors.grade.B; case 'C': - return 'rgba(230, 126, 34, 0.15)'; // #e67e22 with opacity + return theme.colors.grade.C; case 'D': - return 'rgba(231, 76, 60, 0.15)'; // #e74c3c with opacity + return theme.colors.grade.D; + case 'F': + return theme.colors.grade.F; default: - return 'rgba(51, 51, 51, 0.15)'; // #333333 with opacity + return '#333'; } }}; `; -const GradeText = styled.div<{ $grade: string }>` +const GradeText = styled.div` font-size: 20px; font-weight: 600; - color: ${({ $grade }) => { - switch ($grade) { - case 'A': - return '#2ecc71'; - case 'B': - return '#f1c40f'; - case 'C': - return '#e67e22'; - case 'D': - return '#e74c3c'; - default: - return '#333'; - } - }}; + color: ${({ theme }) => theme.colors.grey[100]}; `; const TitleWrapper = styled.div` diff --git a/src/pages/MapPage/components/SearchResult.tsx b/src/pages/MapPage/components/SearchResult.tsx new file mode 100644 index 0000000..85577fc --- /dev/null +++ b/src/pages/MapPage/components/SearchResult.tsx @@ -0,0 +1,102 @@ +import styled from 'styled-components'; + +interface SearchResultProps { + resultCount: number; + gradeCount: { + A: number; + B: number; + C: number; + D: number; + F: number; + }; +} + +const SearchResult = ({ resultCount, gradeCount }: SearchResultProps) => { + return ( + + + 검색결과 {resultCount}건 + + + {Object.entries(gradeCount).map(([grade, count]) => ( + + {grade} + {count} + + ))} + + + ); +}; + +export default SearchResult; + +const Container = styled.div` + display: flex; + align-items: center; + width: 100%; + padding: 10px 25px; + background-color: #f8f9fa; + border-radius: 8px; + margin: 10px 0px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +`; + +const ResultCount = styled.div` + font-size: 15px; + margin-right: 15px; + color: #333; + font-weight: 500; +`; + +const Bold = styled.span` + font-weight: 700; + color: #0b5275; +`; + +const GradeWrapper = styled.div` + display: flex; + gap: 10px; +`; + +interface GradeProps { + grade: string; +} + +const GradeItem = styled.div` + display: flex; + align-items: center; +`; + +const GradeLabel = styled.div` + width: 22px; + height: 22px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-weight: 700; + font-size: 13px; + color: white; + background-color: ${({ grade, theme }) => { + switch (grade) { + case 'A': + return theme.colors.grade.A; + case 'B': + return theme.colors.grade.B; + case 'C': + return theme.colors.grade.C; + case 'D': + return theme.colors.grade.D; + case 'F': + return theme.colors.grade.F; + } + }}; +`; + +const GradeCount = styled.div` + font-size: 14px; + font-weight: 600; + margin-left: 4px; + color: #333; +`; diff --git a/src/pages/MapPage/components/map/MapComponent.tsx b/src/pages/MapPage/components/map/MapComponent.tsx index 5c5584f..a42e06e 100644 --- a/src/pages/MapPage/components/map/MapComponent.tsx +++ b/src/pages/MapPage/components/map/MapComponent.tsx @@ -6,13 +6,14 @@ import { Marker, } from 'react-naver-maps'; import { useState, useEffect } from 'react'; -import AmarkerIcon from '../../../../assets/a.png'; -import BmarkerIcon from '../../../../assets/b.png'; -import CmarkerIcon from '../../../../assets/c.png'; -import DmarkerIcon from '../../../../assets/d.png'; -import FmarkerIcon from '../../../../assets/f.png'; +import AmarkerIcon from '../../../../assets/a.webp'; +import BmarkerIcon from '../../../../assets/b.webp'; +import CmarkerIcon from '../../../../assets/c.webp'; +import DmarkerIcon from '../../../../assets/d.webp'; +import FmarkerIcon from '../../../../assets/f.webp'; import UserPosIcon from '../../../../assets/current_position.png'; import { MapComponentProps } from '../../interface'; +import { MapTypeId, useMapStore } from '../../mapStore'; declare global { interface Window { ReactNativeWebView?: { @@ -32,10 +33,30 @@ const MapComponent: React.FC = ({ setMapInstance, onMarkerClick, }) => { + const { mapTypeId, setIsMapReady } = useMapStore(); const navermaps = useNavermaps(); const [_errorMessage, setErrorMessage] = useState(null); - // console.log(errorMessage); - // console.log(escarpmentData); + + //지도가 준비된 경우 + useEffect(() => { + if (mapInstance) setIsMapReady(true); + }, [mapInstance, setIsMapReady]); + + const getNaverMapTypeId = () => { + if (!navermaps) return undefined; + + switch (mapTypeId) { + case MapTypeId.SATELLITE: + return navermaps.MapTypeId.SATELLITE; + case MapTypeId.HYBRID: + return navermaps.MapTypeId.HYBRID; + case MapTypeId.TERRAIN: + return navermaps.MapTypeId.TERRAIN; + case MapTypeId.NORMAL: + default: + return navermaps.MapTypeId.NORMAL; + } + }; //앱에서 위치 수신 useEffect(() => { @@ -140,6 +161,7 @@ const MapComponent: React.FC = ({ setMapInstance(ref); } }} + mapTypeId={getNaverMapTypeId()} > = ({ {escarpmentData.length > 0 ? escarpmentData.map((item, index) => { // console.log(item); - const grade = item.disaster?.riskLevel.includes('A') + const grade = item.priority?.grade.includes('A') ? 'A' - : item.disaster?.riskLevel.includes('B') + : item.priority?.grade.includes('B') ? 'B' - : item.disaster?.riskLevel.includes('C') + : item.priority?.grade.includes('C') ? 'C' - : item.disaster?.riskLevel.includes('D') + : item.priority?.grade.includes('D') ? 'D' : 'F'; @@ -205,7 +227,7 @@ const MapComponent: React.FC = ({
${ selectedMarkerId === index || allTextShow - ? `
+ ? `
`, diff --git a/src/pages/MapPage/mapStore.ts b/src/pages/MapPage/mapStore.ts new file mode 100644 index 0000000..e19bd83 --- /dev/null +++ b/src/pages/MapPage/mapStore.ts @@ -0,0 +1,286 @@ +// import { create } from 'zustand'; +// import { Slope, slopeMapAPI } from '../../apis/slopeMap'; + +// export interface MapState { +// // 맵 상태 +// selectedMarkerId: number | null; +// allTextShow: boolean; +// userLocation: naver.maps.LatLng | null; +// slopeData: Slope[]; +// searchMod: boolean; +// bottomSheetHeight: number; +// mapInstance: naver.maps.Map | null; +// mapTypeId: naver.maps.MapTypeId; + +// // 액션 +// setSelectedMarkerId: (id: number | null) => void; +// setAllTextShow: (show: boolean) => void; +// setUserLocation: (location: naver.maps.LatLng | null) => void; +// setSlopeData: (data: Slope[]) => void; +// setSearchMod: (mod: boolean) => void; +// setBottomSheetHeight: (height: number) => void; +// setMapInstance: (map: naver.maps.Map | null) => void; +// setMapTypeId: (typeId: naver.maps.MapTypeId) => void; + +// // 비즈니스 로직 +// fetchSlopes: () => Promise; +// handleSearch: (searchValue: string) => void; +// chooseSelectItem: (item: Slope, index: number) => void; +// moveToMyLocation: () => void; +// closeInfo: () => void; +// } + +// export const useMapStore = create((set, get) => ({ +// // 초기 상태 +// selectedMarkerId: null, +// allTextShow: false, +// userLocation: null, +// slopeData: [], +// searchMod: false, +// bottomSheetHeight: 200, +// mapInstance: null, +// mapTypeId: naver.maps.MapTypeId.NORMAL, + +// // 액션 +// setSelectedMarkerId: (id) => set({ selectedMarkerId: id }), +// setAllTextShow: (show) => set({ allTextShow: show }), +// setUserLocation: (location) => set({ userLocation: location }), +// setSlopeData: (data) => set({ slopeData: data }), +// setSearchMod: (mod) => set({ searchMod: mod }), +// setBottomSheetHeight: (height) => set({ bottomSheetHeight: height }), +// setMapInstance: (map) => set({ mapInstance: map }), +// setMapTypeId: (typeId) => set({ mapTypeId: typeId }), + +// // 비즈니스 로직 +// fetchSlopes: async () => { +// const { userLocation } = get(); +// if (!userLocation?.lat() || !userLocation?.lng()) return; + +// try { +// const data = await slopeMapAPI.fetchNearbySlopes( +// userLocation.lat(), +// userLocation.lng() +// ); +// set({ slopeData: data || [] }); +// } catch (error) { +// console.error('Error fetching slopes:', error); +// set({ slopeData: [] }); +// } +// }, + +// handleSearch: async (searchValue) => { +// const { fetchSlopes, userLocation, mapInstance } = get(); + +// if (searchValue === '') { +// set({ searchMod: false, selectedMarkerId: null }); +// fetchSlopes(); +// return; +// } + +// set({ selectedMarkerId: null, searchMod: true }); + +// if (!userLocation?.lat() || !userLocation?.lng()) return; + +// try { +// const data = await slopeMapAPI.searchSlopes( +// searchValue, +// userLocation.lat(), +// userLocation.lng() +// ); +// set({ slopeData: data || [] }); + +// if (mapInstance && data && data.length > 0) { +// const coordinates = data[0].location.coordinates.start.coordinates; +// mapInstance.panTo( +// new naver.maps.LatLng(coordinates[1], coordinates[0]) +// ); +// } +// } catch (error) { +// console.error('Error search slopes:', error); +// set({ slopeData: [] }); +// } +// }, + +// chooseSelectItem: (item, index) => { +// const { mapInstance, selectedMarkerId } = get(); + +// if (mapInstance && item) { +// const coordinates = item.location.coordinates.start.coordinates; +// mapInstance.panTo(new naver.maps.LatLng(coordinates[1], coordinates[0])); + +// set({ selectedMarkerId: selectedMarkerId === index ? null : index }); +// } +// }, + +// moveToMyLocation: () => { +// const { mapInstance, userLocation, fetchSlopes } = get(); + +// if (!mapInstance || !userLocation) return; + +// mapInstance.setZoom(15); + +// setTimeout(() => { +// mapInstance.panTo(userLocation); +// }, 100); + +// fetchSlopes(); +// }, + +// closeInfo: () => { +// set({ selectedMarkerId: null }); +// }, +// })); +import { create } from 'zustand'; +import { Slope, slopeMapAPI } from '../../apis/slopeMap'; + +// 문자열 상수로 맵 타입 정의 +export enum MapTypeId { + NORMAL = 'NORMAL', + SATELLITE = 'SATELLITE', + HYBRID = 'HYBRID', + TERRAIN = 'TERRAIN', +} + +export interface MapState { + // 맵 상태 + selectedMarkerId: number | null; + allTextShow: boolean; + userLocation: naver.maps.LatLng | null; + slopeData: Slope[]; + searchMod: boolean; + bottomSheetHeight: number; + mapInstance: naver.maps.Map | null; + mapTypeId: MapTypeId; + isMapReady: boolean; // 추가: 맵이 완전히 로드되었는지 확인하는 플래그 + + // 액션 + setSelectedMarkerId: (id: number | null) => void; + setAllTextShow: (show: boolean) => void; + setUserLocation: (location: naver.maps.LatLng | null) => void; + setSlopeData: (data: Slope[]) => void; + setSearchMod: (mod: boolean) => void; + setBottomSheetHeight: (height: number) => void; + setMapInstance: (map: naver.maps.Map | null) => void; + setMapTypeId: (typeId: MapTypeId) => void; + setIsMapReady: (isReady: boolean) => void; // 추가: 맵 준비 상태 설정 + + // 비즈니스 로직 + fetchSlopes: () => Promise; + handleSearch: (searchValue: string) => void; + chooseSelectItem: (item: Slope, index: number) => void; + moveToMyLocation: () => void; + closeInfo: () => void; +} + +export const useMapStore = create((set, get) => ({ + // 초기 상태 + selectedMarkerId: null, + allTextShow: false, + userLocation: null, + slopeData: [], + searchMod: false, + bottomSheetHeight: 200, + mapInstance: null, + mapTypeId: MapTypeId.NORMAL, + isMapReady: false, // 초기값: 맵 미준비 상태 + + // 액션 + setSelectedMarkerId: (id) => set({ selectedMarkerId: id }), + setAllTextShow: (show) => set({ allTextShow: show }), + setUserLocation: (location) => set({ userLocation: location }), + setSlopeData: (data) => set({ slopeData: data }), + setSearchMod: (mod) => set({ searchMod: mod }), + setBottomSheetHeight: (height) => set({ bottomSheetHeight: height }), + setMapInstance: (map) => set({ mapInstance: map }), + setMapTypeId: (typeId) => set({ mapTypeId: typeId }), + setIsMapReady: (isReady) => set({ isMapReady: isReady }), + + // 비즈니스 로직 + fetchSlopes: async () => { + const { userLocation, isMapReady } = get(); + // 맵이 준비되지 않았거나 위치 정보가 없으면 중단 + if (!isMapReady || !userLocation?.lat() || !userLocation?.lng()) return; + + try { + const data = await slopeMapAPI.fetchNearbySlopes( + userLocation.lat(), + userLocation.lng() + ); + set({ slopeData: data || [] }); + } catch (error) { + console.error('Error fetching slopes:', error); + set({ slopeData: [] }); + } + }, + + handleSearch: async (searchValue) => { + const { fetchSlopes, userLocation, mapInstance, isMapReady } = get(); + // 맵이 준비되지 않았으면 중단 + if (!isMapReady) return; + + if (searchValue === '') { + set({ searchMod: false, selectedMarkerId: null }); + fetchSlopes(); + return; + } + + set({ selectedMarkerId: null, searchMod: true }); + + if (!userLocation?.lat() || !userLocation?.lng()) return; + + try { + const data = await slopeMapAPI.searchSlopes( + searchValue, + userLocation.lat(), + userLocation.lng() + ); + set({ slopeData: data || [] }); + + if (mapInstance && data && data.length > 0) { + const coordinates = data[0].location.coordinates.start.coordinates; + mapInstance.panTo( + new naver.maps.LatLng(coordinates[1], coordinates[0]) + ); + mapInstance.panTo( + new naver.maps.LatLng(coordinates[1], coordinates[0]) + ); + } + } catch (error) { + console.error('Error search slopes:', error); + set({ slopeData: [] }); + } + }, + + chooseSelectItem: (item, index) => { + const { mapInstance, selectedMarkerId, isMapReady } = get(); + + // 맵이 준비되지 않았으면 중단 + if (!isMapReady || !mapInstance) return; + + if (item) { + const coordinates = item.location.coordinates.start.coordinates; + mapInstance.panTo(new naver.maps.LatLng(coordinates[1], coordinates[0])); + + set({ selectedMarkerId: selectedMarkerId === index ? null : index }); + } + }, + + moveToMyLocation: () => { + const { mapInstance, userLocation, fetchSlopes, isMapReady } = get(); + + // 맵이 준비되지 않았으면 중단 + if (!isMapReady || !mapInstance || !userLocation) return; + + mapInstance.setZoom(15); + + setTimeout(() => { + mapInstance.panTo(userLocation); + }, 100); + + fetchSlopes(); + }, + + closeInfo: () => { + set({ selectedMarkerId: null }); + }, +})); diff --git a/src/styles/theme.ts b/src/styles/theme.ts index 066a964..c6e056e 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -14,6 +14,13 @@ export const theme = { 700: '#333333', }, error: '#CD1A1A', + grade: { + A: '#4CAF50', + B: '#8BC34A', + C: '#FFC107', + D: '#FF9800', + F: '#F44336', + }, }, fonts: { sizes: {