2차 프로젝트 회고

이택우·2022년 5월 31일
0

프로젝트 회고

목록 보기
2/4

Team Project 2 - Poomang

부동산 중개 플랫폼 프로젝트 : 푸망

프로젝트 깃헙 링크


Challenging Points :

  • 오픈 API의 사용법을 익히고, Context API도 동시에 익혀야 했던 점
  • 카카오 지도 API는 DOM에 직접 접근하여 사용해야하기 때문에 여러 컴포넌트에서 발생하는 이벤트 처리가 필요했던 점
  • 지도에서 이벤트가 발생할 때마다 백엔드서버와 통신하여 지도 UI를 업데이트 해야했던 점
  • 형제 컴포넌트 검색/필터링 바의 이벤트 핸들러로 지도 UI를 업데이트 시켜야 했음

지도 컴포넌트 구현

  • kakao map api를 통해 지도를 렌더링하는 함수는 useEffect를 통해 단 한번만 실행되도록 하였습니다.
  • 지도에서 필요한 정보들을 context에 저장하여 리스트, 검색 바에서 지도 정보가 필요할 시 바로 context를 사용해 바로 접근할 수 있도록 하였습니다.
  • 지도 좌표 범위 내매물요청하는 함수
// 지도의 좌표 범위를 보내고, 범위 내의 매물을  
// Context에 저장하는 fetch 함수

  const sendBoundGetItem = () => {
    // 거래 종류(전세, 월세) 필터를 query에 담는 조건문 
    if (
      Object.entries(tradeTypeFilter).filter(el => el[1] === true).length !== 0
    )  {
      tradeTypeQuery = Object.entries(tradeTypeFilter)
        .filter(el => el[1] === true)
        .map(el => el[0])
        .toString();
    }
    fetch(`${BASE_URL}/estates?tradeType=${tradeTypeQuery}`, {
      method: 'GET',
      headers: {
        'Content-type': 'application/json',
        LatLng: `${mapBounds.ha},${mapBounds.oa},${mapBounds.qa},${mapBounds.pa}`,
      },
    })
      .then(res => {
        if (!res.ok) {
          throw new Error(res.statusText);
        }
        return res.json();
      })
      .catch(err => {
        // 응답 에러 시 context의 매물 정보를 빈 배열로 전환
        console.log(err.message)
        RealEstateDispatch({ type: 'GET_REAL_ESTATE', realEstate: [] });
      })
      .then(data => {
        // 해당 범위 내의 존재하는 매물이 없다면 context에 빈 배열로 저장
        // fetch로 받은 매물에서 방종류(원룸, 빌라, 오피스텔, 아파트)를 필터링하는 로직
        if (
          Object.values(RealEstate.roomTypeFilter).filter(
            filter => filter.isOn === true
          ).length < 4
        ) {
          const filteredData = data.clusters.filter(estate =>
            Object.values(RealEstate.roomTypeFilter).find(
              filter => filter.roomType === estate.category_type && filter.isOn
            )
          );
          RealEstateDispatch({
            type: 'GET_REAL_ESTATE',
            realEstate: filteredData,
          });
          return;
        } else {
          RealEstateDispatch({
            type: 'GET_REAL_ESTATE',
            realEstate: data.clusters,
          });
          return;
        }
      });
  };

...

// 지도 범위, 필터, 거래 타입이 업데이트 될 때마다 fetch함수가 실행, 
// context.js에 범위 내 매물 저장
  useEffect(() => {
    sendBoundGetItem();
  }, [mapBounds, roomTypeFilter, tradeTypeFilter]);
  • 클러스터 클릭 이벤트
    • context에 저장된 전체 매물 중에서 클러스터에 포함된 매물과 같은 좌표를 갖는 매물을 필터링하는 이벤트 핸들러.
    • 클릭 후 contextselected 객체에 저장

kakao.maps.event.addListener(clusterer, 'clusterclick', cluster => {
        RealEstateDispatch({
          type: 'GET_SELECTED_ESTATE',
          selected: realEstate.filter(estate => {
            return cluster
              .getMarkers()
              .map(x => x.getPosition())
              .find(
                qa =>
                  estate.lat.toFixed(12) === qa.Ma.toFixed(12) &&
                  estate.lng.toFixed(12) === qa.La.toFixed(12)
              );
          }),
        });
      });

검색 필터링 컴포넌트 구현

  • 카카오 맵 API의 keywordSearch를 사용하여 지역 정보 검색 결과를 가져오게하였습니다.
  • 검색어와 일치하는 주소, 건물 이름을 갖는 매물을 백엔드 서버와 통신하여 결과를 가져오도록 하였습니다.
  • 검색 결과 클릭 시 지도 UI가 업데이트 되도록 하였습니다.
// 주소 검색 콜백 함수
    const placeSearch = (result, status) => {
      if (status === kakao.maps.services.Status.OK) {
        setSearchModal({ ...searchModal, addressResult: result });
        return;
      }
    };
  const searchTextHandler = ({ target }) => {
    if (2 <= target.value.length) {
      setSearchModal({ ...searchModal, isOn: true, searchText: target.value });
      setTradeTypeModal(false);
      setRoomTypeModal(false);
      // 검색 시 주소 검색 결과 배열 state 추가
      places.keywordSearch(target.value, placeSearch);
    } else {
      setSearchModal({
        ...searchModal,
        isOn: false,
        searchText: '',
        addressResult: [],
      });
    }
  };

  // 매물 검색 fetch
  const getSearchResult = () => {
    // 한국어 검색어를 헤더에 넣어서 non ISO-8859-1 code point 에러 발생. / Esint가 자동으로 쉼표를 지우기 때문으로 추정
    // 쿼리 스트링으로 대체
    fetch(
      `${BASE_URL}/estates/content/search?search=${searchModal.searchText}`,
      {
        method: 'GET',
      }
    )
      .then(res => res.json())
      .then(data => {
        setSearchModal({ ...searchModal, searchResult: data });
      });
  };

  // 매물 검색 업데이트 useEffect
  useEffect(() => {
    if (!searchModal.searchText || searchModal.searchText.length < 2) {
      return;
    }
    getSearchResult();
  }, [searchModal.searchText]);

매물 등록 기능 구현

  • 입력 항목 컴포넌트로 분리, context.js로 인풋입력 관리
  • react-daum-postcode api를 모달 창으로 띄워서 주소 업데이트
  • 주소가 업데이트 될 시 kakao map api를 통해 해당 주소로 지도가 업데이트
  • 평 ↔ 제곱미터 변환
  • 예외처리 (빈 입력값, 전용면적이 공급면적보다 클 때, 해당 층수가 건물 층수보다 높을 때)
import React, { useContext } from 'react';
import styled from 'styled-components';
import Header from '../../components/header/Header';
import Footer from '../../components/footer/Footer';
import ManageNav from './ManageNav';
import ManageFormNotice from './ManageFormNotice';
import ManageFormRoomType from './ManageFormRoomType';
import ManageFormAddress from './ManageFormAddress';
import ManageFormSend from './ManageFormSend';
import { GlobalContextProvider } from './context';
import ManageFormDetail from './ManageFormDetail';
import ManageFormRoomInfo from './ManageFormRoomInfo';
import ManageFormTradeType from './ManageFormTradeType';

function ManageForm() {
  return (
    <>
      <Header />
      <Wrapper>
        <TitleWrapper>
          <Title>방내놓기</Title>
        </TitleWrapper>
        <ManageNav select="form" />
        <GlobalContextProvider>
          <ManageFormNotice />
          <ManageFormRoomType />
          <ManageFormAddress />
          <ManageFormTradeType />
          <ManageFormRoomInfo />
          <ManageFormDetail />
          <ManageFormSend />
        </GlobalContextProvider>
      </Wrapper>
      <Footer />
    </>
  );
}

const Wrapper = styled.section`
  margin: 0 auto;
  padding: 0 1rem;
  width: 1200px;
`;
const TitleWrapper = styled.header`
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  width: 100%;
  height: 200px;
`;
const Title = styled.h1`
  font-size: 2rem;
`;

export default ManageForm;

주소 입력에 따라 지도 업데이트

자식 컴포넌트 주소 업데이트/ ManageFormAddress.js

<SearchAddressBox>
              <TextInput
                placeholder="예 ) 번동 10-1, 강북구 번동"
                ref={searchAddressValue}
                onClick={e => (e.target.value = '')}
              />
              <ButtonInput value="주소검색" onClick={handleShowModal} />
            </SearchAddressBox>
            <BorderBox>
              <div className="addressText">
                {' '}
                <span>도로명 : </span>
                {`${infoContext.address_main} ${
                  infoContext.building_name
                    ? `(${infoContext.building_name})`
                    : ''
                }`}
              </div>
              <div className="addressText">
                <span>지 번 : </span>
                {infoContext.jaddress}
              </div>
            </BorderBox>
            <FlexDiv>
              <FlexDiv check={check}>
                <TextInput
                  placeholder="예 ) 101동"
                  check={check}
                  onChange={handleDongAddress}
                />
                <DetailAdressBox marginRight="5px"></DetailAdressBox>
              </FlexDiv>
              <FlexDiv flexWidth={check}>
                <TextInput
                  placeholder="예 ) 101호"
                  onChange={handleHoAddress}
                />
                <DetailAdressBox></DetailAdressBox>
              </FlexDiv>
            </FlexDiv>

ManageAddress의 자식 컴포넌트 - daum-postcode api 모달 창

function ManageFormPostCode({ handle, val }) {
  useEffect(() => {
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = 'auto';
    };
  }, []);

  const infoDispatch = useContext(InfoDispatchContext);
  const handleAddress = data => {
    infoDispatch({ type: 'UPDATE_ADDRESS', address_main: data.address });
    infoDispatch({ type: 'UPDATE_JADDRESS', jaddress: data.jibunAddress });
    infoDispatch({
      type: 'UPDATE_BUILDINGNAME',
      building_name: data.buildingName,
    });
    val.value = data.address;
  };

  return (
    <Outer onClick={handle}>
      <Inner>
        <DaumPostcode onComplete={handleAddress} onClose={handle} />
      </Inner>
    </Outer>
  );
}

postcode 컴포넌트의 주소 검색 결과로 옆의 지도가 업데이트됩니다.

postcode와 형제 컴포넌트인 ManageFormMap.js

function ManageFormMap() {
  const container = useRef(null); //지도를 담을 영역의 DOM 레퍼런스
  const { kakao } = window;
  const infoContext = useContext(InfoContext);
  const infoDispatch = useContext(InfoDispatchContext);
  const Address = infoContext.address_main;
  let Lat = 0;
  let Lng = 0;
  const geocoder = new kakao.maps.services.Geocoder();

  const options = {
    center: new kakao.maps.LatLng(Lat, Lng),
    level: 3,
    draggable: false,
  };
  const marker = new kakao.maps.Marker({
    position: new kakao.maps.LatLng(Lat, Lng),
  });

  useEffect(() => {
    const map = new kakao.maps.Map(container.current, options);
    marker.setMap(map);
    geocoder.addressSearch(
      infoContext.address_main,
      function (results, status) {
        if (status === kakao.maps.services.Status.OK) {
          const result = results[0];
          const coords = new kakao.maps.LatLng(result.y, result.x);
          map.setCenter(coords);
          marker.setPosition(coords);
          infoDispatch({ type: 'UPDATE_LATITUDE', latitude: result.y });
          infoDispatch({ type: 'UPDATE_LONGITUDE', longitude: result.x });
        }
      }
    );
    return () => {};
  }, [Address]);
  return (
    <div
      id="mapDiv"
      ref={container}
      style={{ width: '100%', height: '100%' }}
    />
  );
}

예외 처리

ManageFormRoomInfo.js

전용 면적은 공급 면적보다 작아야합니다.

해당 층수는 건물 층수보다 낮아야 합니다.

const handleExclusive = e => {
    if (supply_size < e) {
      exclusiveSizeMRef.current.value = '';
      alert('전용 면적은 공급 면적보다 클 수 없습니다.');
      return;
    }
    InfoDispatch({
      type: 'UPDATE_EXCLUSIVE_SIZE',
      exclusive_size: e * 1,
    });

...
// 매물 층수가 건물 전체 층수보다 높을 때
<Select
                name="currentFloor"
                required
                onChange={e => {
                  if (
                    building_floor.slice(0, -1) * 1 <
                    e.target.value.slice(0, -1) * 1
                  ) {
                    e.target.value = '';
                    alert('건물 층수보다 높을 수 없습니다.');
                    return;
                  }
                  InfoDispatch({
                    type: 'UPDATE_CURRENT_FLOOR',
                    current_floor: e.target.value,
                  });
                }}
              >

ManageForm

ManageSend.js

반드시 차 있어야 하는 인풋이 비어있다면 send 하지 않습니다.

const sendInfo = () => {
    fetch('http://localhost:8000/estates', {
      method: 'POST',
      headers: { 'Content-type': 'application/json', token: token },
      body: JSON.stringify(Info),
    })
      .then(res => {
        if (!res.ok) {
          throw new Error(res.statusText);
        }
        res.json();
      })
      .catch(err => alert(err))
      .then(alert('매물이 등록되었습니다.'))
      .then(navigate('/manage/list'));
  };

  const verify = () => {
    const {
      address_main,
      address_ho,
      category_id,
      supply_size,
      exclusive_size,
      building_floor,
      current_floor,
      price_main,
      price_deposit,
      price_monthly,
      heat_id,
      available_date,
      description_title,
      description_detail,
      trade_id,
    } = Info;
    if (
      !address_main ||
      !address_ho ||
      !category_id ||
      !supply_size ||
      !exclusive_size ||
      !building_floor ||
      !current_floor ||
      !(price_main || (price_deposit && price_monthly)) ||
      !heat_id ||
      !available_date ||
      !description_title ||
      !description_detail ||
      !trade_id
    ) {
      alert('모든 정보를 입력해주세요');
      return;
    }
    sendInfo();
  };

평 ↔ 제곱미터 전환

ManageFormRoomInfo.js

const PtoM = (e, M) => {
    if (!e.target.value) {
      M.current.value = '';
    } else {
      M.current.value = (e.target.value * 3.3).toFixed(2);
    }
  };
  const MtoP = (e, P) => {
    if (!e.target.value) {
      P.current.value = '';
    } else {
      P.current.value = (e.target.value / 3.3).toFixed(2);
    }
  };
.
.
.
<div className="inner">
            <div className="inner-inner">
              <span>공급 면적</span>
              <input
                ref={supplySizePRef}
                name="supplySizeP"
                type="number"
                onChange={e => {
                  PtoM(e, supplySizeMRef);
                  handleSupply(supplySizeMRef.current.value);
                }}
              />
              <span></span>
              <input
                ref={supplySizeMRef}
                name="supplySize"
                type="number"
                onChange={e => {
                  handleSupply(e.target.value);
                  MtoP(e, supplySizePRef);
                }}
              />
              <span></span>
            </div>

0개의 댓글