본문 바로가기

react

인프런 - MERN 스택으로 만드는 지도 서비스 (+TypeScript) (2)

6. Frontend - 윈포윈도우 생성하기 

 

- 현재 위치를 받고 이를 지도 위에 마커를 통해 표시하는 방법 

 

ㅎㅎ 또날라감 

 

- 인포윈도우 생성 

InfoWindow | 네이버 지도 API v3 (navermaps.github.io)

 

NAVER Maps API v3

NAVER Maps 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 옵션은 실행했을 때 백그라운드에서 실행을 시키겠다는 옵션

뭔지는 모르겠지만 data 폴더 만들어짐

 

- 몽고 디비 서버에 접속할 수 있는 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));