6. Frontend - 윈포윈도우 생성하기
- 현재 위치를 받고 이를 지도 위에 마커를 통해 표시하는 방법
ㅎㅎ 또날라감
- 인포윈도우 생성
InfoWindow | 네이버 지도 API v3 (navermaps.github.io)
InfoWindow/ index.tsx
import { useEffect, useState } from "react";
import { Info } from "../../../types/info";
import "./infoWindow.css";
interface InfoWindowProps {
map: naver.maps.Map;
selectInfo: Info | null;
onSubmit?: () => void;
}
function InfoWindow({ map, selectInfo, onSubmit }: InfoWindowProps) {
const [infoWindow, setInfoWindow] = useState<naver.maps.InfoWindow | null>(
null
);
useEffect(() => {
const _infoWindow = new naver.maps.InfoWindow({
content: "",
backgroundColor: "transparent",
borderWidth: 0,
disableAnchor: true,
pixelOffset: new naver.maps.Point(10, -20),
});
setInfoWindow(_infoWindow);
return () => {
_infoWindow?.setMap(null);
};
}, []);
useEffect(() => {
if (!infoWindow || !map) return;
if (selectInfo) {
infoWindow.setContent(InfoWindowMaker(selectInfo, onSubmit));
infoWindow.open(map, selectInfo.position);
} else {
infoWindow.close();
}
}, [selectInfo]);
return null;
}
function InfoWindowMaker(selectInfo: Info, onSubmit?: () => void) {
const infoWindowBox = document.createElement("div");
infoWindowBox.className = "infoBox";
const infoWindowPlace = document.createElement("div");
infoWindowPlace.className = "infoPlaceName";
infoWindowPlace.innerHTML = `${selectInfo.placeName}`;
infoWindowBox.appendChild(infoWindowPlace);
const infoWindowAddress = document.createElement("div");
infoWindowAddress.className = "infoAddressName";
infoWindowAddress.innerHTML = `${selectInfo.addressName}`;
infoWindowBox.appendChild(infoWindowAddress);
if (onSubmit) {
const infoWindowButton = document.createElement("div");
infoWindowButton.className = "infoSubmit";
infoWindowButton.innerHTML = "등록";
infoWindowButton.onclick = onSubmit;
infoWindowBox.appendChild(infoWindowButton);
}
return infoWindowBox;
}
export default InfoWindow;
2) 인포윈도우 클릭 이벤트 추가
- 맵을 클릭했을떄도 인포윈도우 사라지게
MapContainer.tsx
import React from 'react';
import Map from './common/Map';
import {mapAtom} from '../atoms/map';
import {useSetAtom} from 'jotai';
import { selectInfoAtom } from '../atoms/info';
const MapContainer = () => {
const setMap = useSetAtom(mapAtom);
const setSelectInfo = useSetAtom(selectInfoAtom);
const initMap = (map:naver.maps.Map) => {
setMap(map);
naver.maps.Event.addListener(map, "click", ()=> {
//selectInfo=null
setSelectInfo(null);
});
};
return (
<Map width="100%" height="100%" initMap={initMap}/>
);
};
export default MapContainer;
7. Frontend - Upload 페이지 네비게이션 바 만들기
1) Upload 네비게이션바 만들기
- 기존 네비게이션바를 input 받을 수 있는 상태로 만들기
atom/search.ts
- 인풋 값 전역 상태 관리
import {atom} from "jotai";
export const selectAtom = atom<boolean>(false);
Navigation.tsx
import React from 'react';
import ShadowBox from './common/ShadowBox';
import Button from './common/Button';
import Span from './common/Span';
import Divider from './common/Divider';
import Block from './common/Block';
import {GoPlus} from 'react-icons/go';
interface NavigationProps {
type?: "home" | "upload";
}
const Navigation = ({type="home"} :NavigationProps) => {
return (
<ShadowBox>
<Button type="link" url="/">
<Span size="title">MERN</Span>
</Button>
<Divider width="100px" />
<Block height="28px" />
<Button type="link" url="/upload" >
<GoPlus size="20px" />
</Button>
</ShadowBox>
);
};
export default Navigation;
2) 키워드 검색 창 추가
- 네비게이션 바를 클릭했을 때 input을 받을 수 있는 형태로 바뀔 수 있도록 네비게이션바 코드 변경
Navigation.tsx
import React,{useCallback} from 'react';
import ShadowBox from './common/ShadowBox';
import Button from './common/Button';
import Span from './common/Span';
import Divider from './common/Divider';
import Block from './common/Block';
import Input from './common/Input';
import {GoPlus} from 'react-icons/go';
import {useAtom} from "jotai";
import { selectAtom } from '../atoms/serch';
import { FiArrowLeft } from "react-icons/fi";
import { BiSearch } from "react-icons/bi";
interface NavigationProps {
type?: "home" | "upload";
}
const Navigation = ({type="home"} :NavigationProps) => {
const [select, setSelect] = useAtom(selectAtom);
const onChangeSelect = useCallback (() => {
setSelect(!select);
},[select, setSelect]);
return (
<ShadowBox>
{
type === 'upload' && select ? (
<Button onClick={onChangeSelect}>
<FiArrowLeft size={20} />
</Button>
) : (
<Button type="link" url="/">
<Span size="title">MERN</Span>
</Button>
) }
<Divider width="100px" />
{
select ? (
<Input />
): (
<Block height="28px"
onClick={type === "upload"? onChangeSelect : undefined}
/>
)
}
{type === 'upload'? (
<Button onClick={onChangeSelect}>
<BiSearch size={20} />
</Button>
): (
<Button type="link" url="/upload" >
<GoPlus size="20px" />
</Button>
)}
</ShadowBox>
);
};
export default Navigation;
3) useInput hook 생성 및 적용
- input 컴포넌트를 활용하기 때문에 value를 가져오고 onChange 이벤트도 넘겨줘야 함
- useInput hook을 생성해서 value를 관리하는 기능 추가
hooks/ useInput.tsx
import { useCallback, useState } from "react";
function useInput(initialForm: string) {
const [value, setValue] = useState(initialForm);
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setValue(value);
}, []);
return { value, onChange };
}
export default useInput;
4) 키워드 검색을 위한 onSubmit 함수 생성 및 적용
- Navgation.tsx로 가서 useInput 훅을 가져옴
Navigaton.tsx
import React,{useCallback} from 'react';
import ShadowBox from './common/ShadowBox';
import Button from './common/Button';
import Span from './common/Span';
import Divider from './common/Divider';
import Block from './common/Block';
import Input from './common/Input';
import useInput from '../hooks/useInput';
import {GoPlus} from 'react-icons/go';
import {useAtom} from "jotai";
import { selectAtom } from '../atoms/serch';
import { FiArrowLeft } from "react-icons/fi";
import { BiSearch } from "react-icons/bi";
interface NavigationProps {
type?: "home" | "upload";
}
const Navigation = ({type="home"} :NavigationProps) => {
const [select, setSelect] = useAtom(selectAtom);
const {value, onChange} = useInput('');
const onChangeSelect = useCallback (() => {
setSelect(!select);
},[select, setSelect]);
const onSubmit = useCallback(() => {
console.log(value);
},[value]);
return (
<ShadowBox>
{
type === 'upload' && select ? (
<Button onClick={onChangeSelect}>
<FiArrowLeft size={20} />
</Button>
) : (
<Button type="link" url="/">
<Span size="title">MERN</Span>
</Button>
) }
<Divider width="100px" />
{
select ? (
<Input value={value} onChange={onChange} onSubmit={onSubmit}/>
): (
<Block height="28px"
onClick={type === "upload"? onChangeSelect : undefined}
/>
)
}
{type === 'upload'? (
<Button onClick={select? onSubmit:onChangeSelect}>
<BiSearch size={20} />
</Button>
): (
<Button type="link" url="/upload" >
<GoPlus size="20px" />
</Button>
)}
</ShadowBox>
);
};
export default Navigation;
8. Frontend - SearchBoard 검색 결과창 만들기
1) ResultBox 컴포넌트 소개 및 생성
- 검색창에서 검색 결과를 받았을 때 검색 결과가 나오는 searchBoard 만들기
- 검색결과 하나하나를 resultBox에 담아서 보여줌
- resultBox를 클릭했을 때 해당 데이터로 마커가 이동하고 인포윈도우가 표시되는 기능 만들기
ResultBox.tsx
import React, {memo} from 'react';
import { Info } from '../types/info';
import styled from 'styled-components';
import Span from './common/Span';
interface ResultBoxProps {
info: Info;
onClick:(info:Info) => void;
}
const StyledResultBox = styled.div`
padding: 16px 0;
cursor:pointer;
.place_name {
margin-bottom:6px;
}
`;
const ResultBox = ({info, onClick}:ResultBoxProps) => {
return <StyledResultBox onClick={() => onClick(info)}>
<div className="place_name">
<Span size="title">{info.placeName}</Span>
</div>
<div>
<Span size="small"color="grey">{info.addressName}</Span>
</div>
</StyledResultBox>
};
export default memo(ResultBox);
2) SearchBoard 컴포넌트 소개 및 생성
- 해당 데이터를 받아오는 SearchBoard 만들기
SearchBox.tsx
import React from 'react';
import styled from 'styled-components';
import {useAtom} from "jotai";
import { selectAtom } from '../atoms/serch';
const StyledSearchBoard = styled.div`
width: 100%;
max-width: 436px;
height: 100%;
position: absolute;
background: #ffffff;
top: 0px;
padding: 74px 16px 16px 16px;
`;
const SearchBox = () => {
const [select, setSelect] = useAtom(selectAtom);
//select가 true인 경우에만 렌더링
return <>{select && <StyledSearchBoard></StyledSearchBoard>}</>;
};
export default SearchBox;
3) onSubmit 함수 생성 및 적용
- inputBox에 키워드를 받아온 다음에 submit을 눌렀을 때 검색 결과가 ResultBox로 렌더링 될 수 있도록 만들기
- Navigation.tsx에서 onSubmit 부분 수정
- 현재 infos 데이터를 전역 상태로 관리중
- onSubmit을 했을 때 해당 검색 결과값을 setInfos를 활용해서 infos 데이터를 변경시켜줌
Navigation.tsx
...
const Navigation = ({type="home"} :NavigationProps) => {
const [select, setSelect] = useAtom(selectAtom);
const {value, onChange} = useInput('');
const setInfos = useSetAtom(infosAtom);
const onChangeSelect = useCallback (() => {
setSelect(!select);
},[select, setSelect]);
const onSubmit = useCallback(() => {
setInfos(infos);
},[value]);
...
SearchBox.tsx
import React from 'react';
import styled from 'styled-components';
import {useAtom, useAtomValue} from "jotai";
import { selectAtom } from '../atoms/serch';
import { infosAtom } from '../atoms/info';
import { Info } from "../types/info";
import ResultBox from './ResultBox';
const StyledSearchBoard = styled.div`
width: 100%;
max-width: 436px;
height: 100%;
position: absolute;
background: #ffffff;
top: 0px;
padding: 74px 16px 16px 16px;
.search_board_wrap {
height: 100%;
overflow-y: scroll;
}
.no_result {
text-align: center;
position: relative;
top: 50%;
transform: translateY(-50%);
}
`;
const SearchBox = () => {
const [select, setSelect] = useAtom(selectAtom);
const infos = useAtomValue(infosAtom);
//select가 true인 경우에만 렌더링
return (
<>
{select && (
<StyledSearchBoard>
<div className="search_board_wrap">
{infos && infos.length !== 0 ? (
infos.map((info: Info) => (
<ResultBox
key={info.id}
info={info}
onClick={(info) => {}}
/>
))
) : (
<div className="no_result">검색 결과가 없습니다.</div>
)}
</div>
</StyledSearchBoard>
)}
</>
);
}
export default SearchBox;
4) onClick 함수 생성 및 적용
- 해당 Resultbox를 클릭했을 때 마커가 선택되고 인포윈도우가 표시될 수 있도록 만들기
SearchBox.tsx
import React, {useCallback} from 'react';
import styled from 'styled-components';
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { selectAtom } from '../atoms/serch';
import { infosAtom, selectInfoAtom } from '../atoms/info';
import { Info } from "../types/info";
import ResultBox from './ResultBox';
const StyledSearchBoard = styled.div`
width: 100%;
max-width: 436px;
height: 100%;
position: absolute;
background: #ffffff;
top: 0px;
padding: 74px 16px 16px 16px;
.search_board_wrap {
height: 100%;
overflow-y: scroll;
}
.no_result {
text-align: center;
position: relative;
top: 50%;
transform: translateY(-50%);
}
`;
const SearchBox = () => {
const [select, setSelect] = useAtom(selectAtom);
const infos = useAtomValue(infosAtom);
const setSelectInfo = useSetAtom(selectInfoAtom);
const onClickResultBox = useCallback ((info:Info) => {
setSelectInfo(info);
setSelect(!select);
},[select,setSelect, setSelectInfo]);
//select가 true인 경우에만 렌더링
return (
<>
{select && (
<StyledSearchBoard>
<div className="search_board_wrap">
{infos && infos.length !== 0 ? (
infos.map((info: Info) => (
<ResultBox
key={info.id}
info={info}
onClick={onClickResultBox}
/>
))
) : (
<div className="no_result">검색 결과가 없습니다.</div>
)}
</div>
</StyledSearchBoard>
)}
</>
);
}
export default SearchBox;
MarkerContainer.tsx
interface MarkersContainerProps {
type?: "home" | "upload";
}
const MarkersContainer = ({type="home"}:MarkersContainerProps) => {
const map = useAtomValue(mapAtom);
const infos = useAtomValue(infosAtom);
const [selectInfo, setSelectInfo] = useAtom(selectInfoAtom);
const onSubmit = useCallback(()=> {
console.log("제출!");
},[]);
if(!map ||!infos) return null;
return (
<>
{infos.map((info:Info) => (
<Marker
key={info.id}
map={map}
position={info.position}
content={'<div class="marker" />'}
onClick={() => {
setSelectInfo(info);
}}
/>
))}
{selectInfo && (
<Marker
key={selectInfo.id}
map={map}
position={selectInfo.position}
content={'<div class="marker select"/>'}
onClick={() => {
setSelectInfo(null);
}}
/>
)}
<InfoWindow map={map} selectInfo={selectInfo} onSubmit={type === "upload"? onSubmit : undefined}
/>
</>
);
};
export default MarkersContainer;
upload/index.tsx
...
return (
<>
<Navigation type="upload" />
<MapContainer />
<MarkersContainer type="upload"/>
<SearchBox />
</>
)
}
...
9. DataBase - MongoDB
1) MongoDB 및 Mongoose 초기 세팅
- docker-compose를 활용해서 로컬환경에 MongoDB 서버를 실행
- docker-compose란 docker-compose.yml 파일을 활용해서 여러 개의 컨테이너를 동시에 실행하고 관리할 수 있는 방법
- docker desktop 설치 후 터미널에 docker -v 한 후 doker 버전이 나오면 제대로 설치된 것
- Docker Compose -v 하고 버전 정보가 나오면 성공적으로 설치
- 터미널에 cd mern_mongo로 폴더 위치 이동
- docker-compose up -d , 해당 up 명령어는 해당 docker-compose.yml 바탕으로 컨테이너를 실행시키겠다는 의미
d 옵션은 실행했을 때 백그라운드에서 실행을 시키겠다는 옵션
- 몽고 디비 서버에 접속할 수 있는 URI 생성
- 백엔드쪽에서 몽고 디비 서버를 연결하는 방법
import app from '../app';
import debug from 'debug';
import http from 'http';
import mongoose from 'mongoose';
/**
* Get port from environment and store in Express.
*/
const port = normalizePort('3001');
app.set('port', port);
mongoose.connect(
`mongodb://mern:merntest@localhost:27017/?authMechanism=DEFAULT&authSource=admin`
).then(() => console.log("Connected to mongo server"))
.catch((e) => console.log(e));
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 12장 immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2023.11.16 |
---|---|
[리액트를 다루는 기술] 11장 컴포넌트 성능 최적화 (0) | 2023.11.15 |
인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (1) (3) | 2023.11.10 |
[리액트를 다루는 기술] 10장 일정 관리 웹 애플리케이션 만들기 (0) | 2023.11.08 |
[리액트를 다루는 기술] 8장 Hooks (2) | 2023.11.03 |