프론트엔드 코드 리뷰 - Reactjs, Nodejs, Python을 이용하여 대학생 자취 지역 추천 서비스 만들기

Design.C·2022년 1월 4일
1

프론트엔드에서 복습해 본 핵심 키워드

https://github.com/znehraks/unibangcity-frontend

  • 실시간 크롤링 데이터 fetch를 통한 서비스

  • kakaoMap api 활용

  • 데이터 시각화 라이브러리 react-chartjs-2 활용

  • react-router-dom 활용

디렉토리 구조



중 살펴볼 코드는

  • Frontend/components/kakao 하위 코드

  • Frontend/components/recommendationMode 하위 코드

  • Frontend/components/App.js

  • Frontend/components/ArticleButton.js

  • Frontend/components/RouterComponents.js

  • Frontend/screens/Recommendation/ 하위 코드

App.js

최상위 컴포넌트인 App.js이다.

  • GlobalStyles로 초기 디자인 세팅을 하였다.
  • BrowserRouter로 라우팅을 구현했다.
  • RouterComponent에 url별로 노출될 컴포넌트를 설정했다.
// /Frontend/components/App.js

import React from "react";
import { BrowserRouter } from "react-router-dom";
import styled, { ThemeProvider } from "styled-components";
import Footer from "./Footer";
import Header from "./Header";
import RouterComponent from "./RouterComponent";
import GlobalStyle from "./styles/GlobalStyles";
import Theme from "./styles/Theme";

const Wrapper = styled.div`
  width: 100vw;
  height: 100vh;
  background: #fff;
`;
const App = () => {
  return (
    <ThemeProvider theme={Theme}>
      <GlobalStyle />
      <Wrapper>
        <BrowserRouter>
          <Header />
          <RouterComponent />
          <Footer />
        </BrowserRouter>
      </Wrapper>
    </ThemeProvider>
  );
};

export default App;

recommendationMode/Intro.js

추천페이지의 인트로 페이지를 담당하는 컴포넌트이다.

  • "다음"을 누르게 되면 setMode 훅에 의해 1단계로 넘어간다.

import { Q1 } from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
  TextArticle,
  TextArticleSpan,
} from "../styles/StyledComponents";
import PropTypes from "prop-types";
const Intro = ({ setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>유니방시티 이용 안내입니다.</MainTitle>
        <MainSubTitleSpan>
          아래 내용을 숙지해주시고 '다음'버튼을 눌러주세요
        </MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer flexDirection={"column"}>
        <TextArticle width={"35%"} height={"70%"} lineHeight="0.2vw">
          <TextArticleSpan>1. 나의 학교 이름을 입력해 주세요.</TextArticleSpan>
          <TextArticleSpan>
            2. 원하는 최대 거리를 선택해 주세요.
          </TextArticleSpan>
          <TextArticleSpan>
            3. 가장 많이 고려하는 요소를 1개 골라주세요.
          </TextArticleSpan>
          <TextArticleSpan>
            4. 두 번째로 많이 고려하는 요소를 1개 골라주세요.
          </TextArticleSpan>
          <TextArticleSpan>
            5. 세 번째로 많이 고려하는 요소를 1개 골라주세요.
          </TextArticleSpan>
          <TextArticleSpan>6. 조금만 기다리면 끝.</TextArticleSpan>
        </TextArticle>
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(Q1);
          }}
        >
          다음
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Intro;

Intro.propTypes = {
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Q1.js

추천페이지의 1단계 페이지를 담당하는 컴포넌트이다.

  • 검색어를 input에 입력하고, 밑에 자동완성되어 나오는 단어를 누르게 되면, Q1이 선택된다.

  • 선택된 대학교의 이름을 key로 하여, 해당 대학교의 주소(위도,경도)를 universityList.js에서 가져오고, 이 위도,경도,대학 이름을 answers에 state로 등록한다.

import UniversityList from "../data/universityList";
import { INTRO, Q2 } from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  HiddenSearchBox,
  HiddenSearchLine,
  Input,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import PropTypes from "prop-types";
const Q1Component = ({
  schoolNameInputRef,
  schoolNameInput,
  setAnswers,
  answers,
  setMode,
}) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>1. 나의 학교를 선택해주세요.</MainTitle>
        <MainSubTitleSpan>
          ex. '명지대학교'검색 시 '명지' 입력 후 아래에서 선택
        </MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer flexDirection="column" justifyContent="center">
        <Input
          ref={schoolNameInputRef}
          autoFocus
          type="text"
          placeholder="ex. 명지대학교"
          {...schoolNameInput}
//클릭 시, answers.Q1answer, answers.univ_lat, answers.univ_lon을 비워준다.
          onClick={() => {
            setAnswers({
              ...answers,
              Q1Answer: "",
              univ_lat: "",
              univ_lon: "",
            });
            //schoolNameInput의 value도 비워준다.
            schoolNameInput.setValue("");
          }}
        />
//answers.Q1Answer이 공백이라면(선택되지 않았다면)
        {answers.Q1Answer === "" && (
          <HiddenSearchBox valueLength={schoolNameInput.value.length}>
          //UniversityList에서 input에 입력된 글자를 갖고 있는 대학교가 있다면
            {UniversityList.map((item) => {
              if (
                item.name.includes(schoolNameInput.value) &&
                schoolNameInput.value !== ""
              ) {
                //그 대학교 이름을 map으로 생성된 컴포넌트에 노출함
                return (
                  <HiddenSearchLine
                    key={item.name}
                    onClick={() => {
                      setAnswers({
                        ...answers,
                        Q1Answer: item.name,
                        univ_lat: item.address_lat,
                        univ_lon: item.address_lon,
                      });
                      schoolNameInput.setValue(item.name);
                    }}
                  >
                    {item.name}
                  </HiddenSearchLine>
                );
              } else {
                return null;
              }
            })}
          </HiddenSearchBox>
        )}
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(INTRO);
            setAnswers({ ...answers, Q1Answer: "" });
          }}
        >
          이전
        </ButtonBox>
//answers.Q1Answer의 공백체크 후 다음 단계로 넘김
        <ButtonBox
          onClick={() => {
            if (answers.Q1Answer !== "") {
              setMode(Q2);
              schoolNameInput.setValue("");
//answers.Q1Answer가 공백이라면 ref훅을 이용하여, input에 focus시킴.
            } else {
              alert("학교를 선택해주세요.");
              schoolNameInputRef.current.focus();
            }
          }}
        >
          다음
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Q1Component;

Q1Component.propTypes = {
  schoolNameInputRef: PropTypes.object.isRequired,
  schoolNameInput: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  answers: PropTypes.object.isRequired,
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Q2.js

추천페이지의 2단계 페이지를 담당하는 컴포넌트이다.

  • 카카오맵api를 통해 원 반경 거리를 입력받아 answers.Q2Answer에 저장한다.

import { Q1, Q3 } from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import Q2Map from "../kakao/Q2Map";
import PropTypes from "prop-types";
const Q2Component = ({ answers, setAnswers, setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>2. 원하는 거리를 선택해주세요.</MainTitle>
        <MainSubTitleSpan>
          지도를 마우스로 클릭하면 원 그리기가 시작되고 마우스 우클릭하면 원
          그리기가 종료됩니다.
        </MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer>
        <Q2Map
          answers={answers}
          setAnswers={setAnswers}
          mobile={false}
          univ_lat={answers.univ_lat}
          univ_lon={answers.univ_lon}
        />
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(Q1);
            setAnswers({ ...answers, Q2Answer: "" });
          }}
        >
          이전
        </ButtonBox>
        <ButtonBox
          onClick={() => {
            if (answers.Q2Answer !== "") {
              setMode(Q3);
            } else {
              alert("거리를 설정해주세요.");
            }
          }}
        >
          다음
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Q2Component;
Q2Component.propTypes = {
  answers: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  setMode: PropTypes.func.isRequired,
};

Q2Map.js

Q2 컴포넌트에 쓰인 카카오맵의 코드이다

  • Q1에서 선택된 대학교의 위도,경도를 중심으로 원을 그려 반지름을 Q2Answer로 정한다.

  • 다른 부분은 크게 건들이지 않고 리액트에 맞게 변형하고, 훅을 추가했다.

import React, { useEffect } from "react";
const { kakao } = window;

const MapContainer = ({ answers, univ_lat, univ_lon, setAnswers, mobile }) => {
  useEffect(() => {
    //지도 넣을 컨테이너
    const container = document.getElementById("myMap");

    // 지도에 들어가는 옵션
    const options = {
      center: new kakao.maps.LatLng(univ_lat, univ_lon),
      level: 6,
      draggable: true,
      scrollwheel: true,
    };

    //지도 객체 생성
    const map = new kakao.maps.Map(container, options);

    let drawingFlag = false; // 원이 그려지고 있는 상태를 가지고 있을 변수입니다
    let centerPosition; // 원의 중심좌표 입니다
    let drawingCircle; // 그려지고 있는 원을 표시할 원 객체입니다
    let drawingLine; // 그려지고 있는 원의 반지름을 표시할 선 객체입니다
    let drawingOverlay; // 그려지고 있는 원의 반경을 표시할 커스텀오버레이 입니다
    let drawingDot; // 그려지고 있는 원의 중심점을 표시할 커스텀오버레이 입니다

    let circles = [];

    // 지도에 클릭 이벤트를 등록합니다
    kakao.maps.event.addListener(map, "click", function (mouseEvent) {
      // 클릭 이벤트가 발생했을 때 원을 그리고 있는 상태가 아니면 중심좌표를 클릭한 지점으로 설정합니다
      if (!drawingFlag) {
        if (circles.length !== 0) {
          removeCircles();
        }
        // 상태를 그리고있는 상태로 변경합니다
        drawingFlag = true;

        // 원이 그려질 중심좌표를 클릭한 위치로 설정합니다
        centerPosition = options.center;
        console.log(centerPosition);

        // 그려지고 있는 원의 반경을 표시할 선 객체를 생성합니다
        if (!drawingLine) {
          drawingLine = new kakao.maps.Polyline({
            strokeWeight: 3, // 선의 두께입니다
            strokeColor: "#00a0e9", // 선의 색깔입니다
            strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
            strokeStyle: "solid", // 선의 스타일입니다
          });
        }

        // 그려지고 있는 원을 표시할 원 객체를 생성합니다
        if (!drawingCircle) {
          drawingCircle = new kakao.maps.Circle({
            strokeWeight: 1, // 선의 두께입니다
            strokeColor: "#00a0e9", // 선의 색깔입니다
            strokeOpacity: 0.1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
            strokeStyle: "solid", // 선의 스타일입니다
            fillColor: "#00a0e9", // 채우기 색깔입니다
            fillOpacity: 0.2, // 채우기 불투명도입니다
          });
        }

        // 그려지고 있는 원의 반경 정보를 표시할 커스텀오버레이를 생성합니다
        if (!drawingOverlay) {
          drawingOverlay = new kakao.maps.CustomOverlay({
            xAnchor: 0,
            yAnchor: 0,
            zIndex: 1,
          });
        }
      }
    });

    // 지도에 마우스무브 이벤트를 등록합니다
    // 원을 그리고있는 상태에서 마우스무브 이벤트가 발생하면 그려질 원의 위치와 반경정보를 동적으로 보여주도록 합니다
    kakao.maps.event.addListener(map, "mousemove", function (mouseEvent) {
      // 마우스무브 이벤트가 발생했을 때 원을 그리고있는 상태이면
      if (drawingFlag) {
        // 마우스 커서의 현재 위치를 얻어옵니다
        var mousePosition = mouseEvent.latLng;

        // 그려지고 있는 선을 표시할 좌표 배열입니다. 클릭한 중심좌표와 마우스커서의 위치로 설정합니다
        var linePath = [centerPosition, mousePosition];

        // 그려지고 있는 선을 표시할 선 객체에 좌표 배열을 설정합니다
        drawingLine.setPath(linePath);

        // 원의 반지름을 선 객체를 이용해서 얻어옵니다
        var length = drawingLine.getLength();

        if (length > 0) {
          // 그려지고 있는 원의 중심좌표와 반지름입니다
          var circleOptions = {
            center: centerPosition,
            radius: length,
          };

          // 그려지고 있는 원의 옵션을 설정합니다
          drawingCircle.setOptions(circleOptions);

          // 반경 정보를 표시할 커스텀오버레이의 내용입니다
          var radius = Math.round(drawingCircle.getRadius()),
            content =
              '<div class="info">반경 <span class="number">' +
              radius +
              "</span>m</div>";

          // 반경 정보를 표시할 커스텀 오버레이의 좌표를 마우스커서 위치로 설정합니다
          drawingOverlay.setPosition(mousePosition);

          // 반경 정보를 표시할 커스텀 오버레이의 표시할 내용을 설정합니다
          drawingOverlay.setContent(content);

          // 그려지고 있는 원을 지도에 표시합니다
          drawingCircle.setMap(map);

          // 그려지고 있는 선을 지도에 표시합니다
          drawingLine.setMap(map);

          // 그려지고 있는 원의 반경정보 커스텀 오버레이를 지도에 표시합니다
          drawingOverlay.setMap(map);
        } else {
          drawingCircle.setMap(null);
          drawingLine.setMap(null);
          drawingOverlay.setMap(null);
        }
      }
    });
    // 지도에 마우스 오른쪽 클릭이벤트를 등록합니다
    // 원을 그리고있는 상태에서 마우스 오른쪽 클릭 이벤트가 발생하면
    // 마우스 오른쪽 클릭한 위치를 기준으로 원과 원의 반경정보를 표시하는 선과 커스텀 오버레이를 표시하고 그리기를 종료합니다
    kakao.maps.event.addListener(map, "rightclick", function (mouseEvent) {
      if (drawingFlag) {
        // 마우스로 오른쪽 클릭한 위치입니다
        var rClickPosition = mouseEvent.latLng;

        // 원의 반경을 표시할 선 객체를 생성합니다
        var polyline = new kakao.maps.Polyline({
          path: [centerPosition, rClickPosition], // 선을 구성하는 좌표 배열입니다. 원의 중심좌표와 클릭한 위치로 설정합니다
          strokeWeight: 3, // 선의 두께 입니다
          strokeColor: "#00a0e9", // 선의 색깔입니다
          strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
          strokeStyle: "solid", // 선의 스타일입니다
        });

        // 원 객체를 생성합니다
        var circle = new kakao.maps.Circle({
          center: centerPosition, // 원의 중심좌표입니다
          radius: polyline.getLength(), // 원의 반지름입니다 m 단위 이며 선 객체를 이용해서 얻어옵니다
          strokeWeight: 1, // 선의 두께입니다
          strokeColor: "#00a0e9", // 선의 색깔입니다
          strokeOpacity: 0.1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
          strokeStyle: "solid", // 선의 스타일입니다
          fillColor: "#00a0e9", // 채우기 색깔입니다
          fillOpacity: 0.2, // 채우기 불투명도입니다
        });

        var radius = Math.round(circle.getRadius()), // 원의 반경 정보를 얻어옵니다
          content = getTimeHTML(radius); // 커스텀 오버레이에 표시할 반경 정보입니다

        //setAnswers 훅을 통해 Q2Answer에 원 반경거리를 설정한다.
        setAnswers({ ...answers, Q2Answer: radius });

        // 반경정보를 표시할 커스텀 오버레이를 생성합니다
        var radiusOverlay = new kakao.maps.CustomOverlay({
          content: content, // 표시할 내용입니다
          position: rClickPosition, // 표시할 위치입니다. 클릭한 위치로 설정합니다
          xAnchor: 0,
          yAnchor: 0,
          zIndex: 1,
        });

        // 원을 지도에 표시합니다
        circle.setMap(map);

        // 선을 지도에 표시합니다
        polyline.setMap(map);

        // 반경 정보 커스텀 오버레이를 지도에 표시합니다
        radiusOverlay.setMap(map);

        // 배열에 담을 객체입니다. 원, 선, 커스텀오버레이 객체를 가지고 있습니다
        var radiusObj = {
          polyline: polyline,
          circle: circle,
          overlay: radiusOverlay,
        };

        // 배열에 추가합니다
        // 이 배열을 이용해서 "모두 지우기" 버튼을 클릭했을 때 지도에 그려진 원, 선, 커스텀오버레이들을 지웁니다
        circles.push(radiusObj);

        // 그리기 상태를 그리고 있지 않는 상태로 바꿉니다
        drawingFlag = false;

        // 중심 좌표를 초기화 합니다
        centerPosition = null;

        // 그려지고 있는 원, 선, 커스텀오버레이를 지도에서 제거합니다
        drawingCircle.setMap(null);
        drawingLine.setMap(null);
        drawingOverlay.setMap(null);
      }
    });

    // 지도에 표시되어 있는 모든 원과 반경정보를 표시하는 선, 커스텀 오버레이를 지도에서 제거합니다
    function removeCircles() {
      for (var i = 0; i < circles.length; i++) {
        circles[i].circle.setMap(null);
        circles[i].polyline.setMap(null);
        circles[i].overlay.setMap(null);
      }
      circles = [];
    } // 마우스 우클릭 하여 원 그리기가 종료됐을 때 호출하여
    // 그려진 원의 반경 정보와 반경에 대한 도보, 자전거 시간을 계산하여
    // HTML Content를 만들어 리턴하는 함수입니다
    function getTimeHTML(distance) {
      // 도보의 시속은 평균 4km/h 이고 도보의 분속은 67m/min입니다
      var walkkTime = (distance / 67) | 0;
      var walkHour = "",
        walkMin = "";

      // 계산한 도보 시간이 60분 보다 크면 시간으로 표시합니다
      if (walkkTime > 60) {
        walkHour =
          '<span class="number">' + Math.floor(walkkTime / 60) + "</span>시간 ";
      }
      walkMin = '<span class="number">' + (walkkTime % 60) + "</span>분";

      // 자전거의 평균 시속은 16km/h 이고 이것을 기준으로 자전거의 분속은 267m/min입니다
      var bycicleTime = (distance / 227) | 0;
      var bycicleHour = "",
        bycicleMin = "";

      // 계산한 자전거 시간이 60분 보다 크면 시간으로 표출합니다
      if (bycicleTime > 60) {
        bycicleHour =
          '<span class="number">' +
          Math.floor(bycicleTime / 60) +
          "</span>시간 ";
      }
      bycicleMin = '<span class="number">' + (bycicleTime % 60) + "</span>분";

      // 거리와 도보 시간, 자전거 시간을 가지고 HTML Content를 만들어 리턴합니다
      var content = '<ul class="info">';
      content += "    <li>";
      content +=
        '        <span class="label">총거리</span><span class="number">' +
        distance +
        "</span>m";
      content += "    </li>";
      content += "    <li>";
      content += '        <span class="label">도보</span>' + walkHour + walkMin;
      content += "    </li>";
      content += "    <li>";
      content +=
        '        <span class="label">자전거</span>' + bycicleHour + bycicleMin;
      content += "    </li>";
      content += "</ul>";

      return content;
    }
  }, []);

  return (
    <div
      id="myMap"
      style={{
        width: mobile ? "60vw" : "22vw",
        height: mobile ? "60vw" : "22vw",
      }}
    ></div>
  );
};

export default MapContainer;

recommendationMode/Q3~Q5.js

추천페이지의 3,4,5단계 페이지를 담당하는 컴포넌트이다.

  • 1순위로 중요하게 여기는 요소를 택하여 Q3Answer에 할당한다.

  • 2순위로 중요하게 여기는 요소를 택하여 Q4Answer에 할당한다.

  • 3순위로 중요하게 여기는 요소를 택하여 Q5Answer에 할당한다.

  • Q5에서는 모든 문항의 유효성 체크를 한다. 공백이 있거나 입력되지 않은 문항이 있으면 해당 문항으로 mode를 변환한 뒤, 입력을 유도한다.

import ArticleButton from "../ArticleButton";
import { COST, DISTANCE, HOUSE, Q2, Q3, Q4, SAFETY, SUBWAY } from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import distance_img from "../styles/images/distance.png";
import subway_img from "../styles/images/subway.png";
import cost_img from "../styles/images/cost.png";
import safety_img from "../styles/images/safety.png";
import house_img from "../styles/images/house.png";
import distance_red_img from "../styles/images/distance_red.png";
import subway_red_img from "../styles/images/subway_red.png";
import cost_red_img from "../styles/images/cost_red.png";
import safety_red_img from "../styles/images/safety_red.png";
import house_red_img from "../styles/images/house_red.png";
import PropTypes from "prop-types";
const Q3Component = ({ answers, setAnswers, setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>3. 가장 중요시 여기는 요소를 선택해주세요.</MainTitle>
        <MainSubTitleSpan>아래 항목 중 한 개만 선택해 주세요.</MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer>
        <ArticleButton
          current={Q3}
          name={DISTANCE}
          kr_name={"거리"}
          black_img={distance_img}
          red_img={distance_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q3Answer === DISTANCE}
        />
        <ArticleButton
          current={Q3}
          name={SUBWAY}
          kr_name={"역세권"}
          black_img={subway_img}
          red_img={subway_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q3Answer === SUBWAY}
        />
        <ArticleButton
          current={Q3}
          name={COST}
          kr_name={"가성비"}
          black_img={cost_img}
          red_img={cost_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q3Answer === COST}
        />
        <ArticleButton
          current={Q3}
          name={SAFETY}
          kr_name={"안전"}
          black_img={safety_img}
          red_img={safety_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q3Answer === SAFETY}
        />
        <ArticleButton
          current={Q3}
          name={HOUSE}
          kr_name={"주변 매물 수"}
          black_img={house_img}
          red_img={house_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q3Answer === HOUSE}
        />
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(Q2);
            setAnswers({ ...answers, Q3Answer: "", Q3Answer_kr: "" });
          }}
        >
          이전
        </ButtonBox>
        <ButtonBox
          onClick={() => {
            if (answers.Q3Answer !== "") {
              setMode(Q4);
            } else {
              alert("항목을 선택해주세요.");
            }
          }}
        >
          다음
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Q3Component;

Q3Component.propTypes = {
  answers: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Q4.js

import ArticleButton from "../ArticleButton";
import { COST, DISTANCE, HOUSE, Q3, Q4, Q5, SAFETY, SUBWAY } from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import distance_img from "../styles/images/distance.png";
import subway_img from "../styles/images/subway.png";
import cost_img from "../styles/images/cost.png";
import safety_img from "../styles/images/safety.png";
import house_img from "../styles/images/house.png";
import distance_red_img from "../styles/images/distance_red.png";
import subway_red_img from "../styles/images/subway_red.png";
import cost_red_img from "../styles/images/cost_red.png";
import safety_red_img from "../styles/images/safety_red.png";
import house_red_img from "../styles/images/house_red.png";

import PropTypes from "prop-types";
const Q4Component = ({ answers, setAnswers, setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>4. 두 번째로 중요시 여기는 요소를 선택해주세요.</MainTitle>
        <MainSubTitleSpan>아래 항목 중 한 개만 선택해 주세요.</MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer>
        <ArticleButton
          current={Q4}
          name={DISTANCE}
          kr_name={"거리"}
          black_img={distance_img}
          red_img={distance_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q4Answer === DISTANCE}
        />
        <ArticleButton
          current={Q4}
          name={SUBWAY}
          kr_name={"역세권"}
          black_img={subway_img}
          red_img={subway_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q4Answer === SUBWAY}
        />
        <ArticleButton
          current={Q4}
          name={COST}
          kr_name={"가성비"}
          black_img={cost_img}
          red_img={cost_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q4Answer === COST}
        />
        <ArticleButton
          current={Q4}
          name={SAFETY}
          kr_name={"안전"}
          black_img={safety_img}
          red_img={safety_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q4Answer === SAFETY}
        />
        <ArticleButton
          current={Q4}
          name={HOUSE}
          kr_name={"주변 매물 수"}
          black_img={house_img}
          red_img={house_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q4Answer === HOUSE}
        />
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(Q3);
            setAnswers({ ...answers, Q4Answer: "", Q4Answer_kr: "" });
          }}
        >
          이전
        </ButtonBox>
        <ButtonBox
          onClick={() => {
            if (answers.Q4Answer !== "") {
              setMode(Q5);
            } else {
              alert("항목을 선택해주세요.");
            }
          }}
        >
          다음
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Q4Component;

Q4Component.propTypes = {
  answers: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Q5.js

import ArticleButton from "../ArticleButton";
import {
  COST,
  DISTANCE,
  FINISH,
  HOUSE,
  Q1,
  Q2,
  Q3,
  Q4,
  Q5,
  SAFETY,
  SUBWAY,
} from "../Enum";
import {
  ButtonBox,
  ButtonContainer,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import distance_img from "../styles/images/distance.png";
import subway_img from "../styles/images/subway.png";
import cost_img from "../styles/images/cost.png";
import safety_img from "../styles/images/safety.png";
import house_img from "../styles/images/house.png";
import distance_red_img from "../styles/images/distance_red.png";
import subway_red_img from "../styles/images/subway_red.png";
import cost_red_img from "../styles/images/cost_red.png";
import safety_red_img from "../styles/images/safety_red.png";
import house_red_img from "../styles/images/house_red.png";

import PropTypes from "prop-types";
const Q5Component = ({ answers, setAnswers, setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>5. 세 번째로 중요시 여기는 요소를 선택해주세요.</MainTitle>
        <MainSubTitleSpan>아래 항목 중 한 개만 선택해 주세요.</MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer>
        <ArticleButton
          current={Q5}
          name={DISTANCE}
          kr_name={"거리"}
          black_img={distance_img}
          red_img={distance_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q5Answer === DISTANCE}
        />
        <ArticleButton
          current={Q5}
          name={SUBWAY}
          kr_name={"역세권"}
          black_img={subway_img}
          red_img={subway_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q5Answer === SUBWAY}
        />
        <ArticleButton
          current={Q5}
          name={COST}
          kr_name={"가성비"}
          black_img={cost_img}
          red_img={cost_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q5Answer === COST}
        />
        <ArticleButton
          current={Q5}
          name={SAFETY}
          kr_name={"안전"}
          black_img={safety_img}
          red_img={safety_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q5Answer === SAFETY}
        />
        <ArticleButton
          current={Q5}
          name={HOUSE}
          kr_name={"주변 매물 수"}
          black_img={house_img}
          red_img={house_red_img}
          answers={answers}
          setAnswers={setAnswers}
          isSelected={answers.Q5Answer === HOUSE}
        />
      </MainArticleContainer>
      <ButtonContainer>
        <ButtonBox
          onClick={() => {
            setMode(Q4);

            setAnswers({ ...answers, Q5Answer: "", Q5Answer_kr: "" });
          }}
        >
          이전
        </ButtonBox>
//유효성 체크
        <ButtonBox
          onClick={() => {
            if (answers.Q1Answer === "") {
              alert("1단계가 완료되지 않았습니다.");
              setMode(Q1);
            } else if (answers.Q2Answer === "") {
              alert("2단계가 완료되지 않았습니다.");
              setMode(Q2);
            } else if (answers.Q3Answer === "") {
              alert("3단계가 완료되지 않았습니다.");
              setMode(Q3);
            } else if (answers.Q4Answer === "") {
              alert("4단계가 완료되지 않았습니다.");
              setMode(Q4);
            } else if (answers.Q5Answer === "") {
              alert("항목을 선택해주세요.");
              setMode(Q5);
            } else {
              setMode(FINISH);
            }
          }}
        >
          완료
        </ButtonBox>
      </ButtonContainer>
    </>
  );
};

export default Q5Component;

Q5Component.propTypes = {
  answers: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Finish.js

모든 문항 입력이 완료되고, 결과를 보기 직전 페이지이다.

  • 크롤링을 통해 실시간으로 제공하는 서비스이다 보니, 속도가 다소 느린 점을 해소하기 위해, 이 페이지에서 fetch를 시작하며 시간을 버는 형태로 구현했다.

  • 결과보기를 누르면 다음으로 넘어간다.

import { RESULT } from "../Enum";
import {
  MainArticle,
  MainArticleContainer,
  MainSubTitleSpan,
  MainTitle,
  MainTitleContainer,
} from "../styles/StyledComponents";
import PropTypes from "prop-types";
const Finish = ({ setMode }) => {
  return (
    <>
      <MainTitleContainer>
        <MainTitle>나만의 자취방을 보러가실 시간입니다.</MainTitle>
        <MainSubTitleSpan>아래 버튼을 눌러주세요.</MainSubTitleSpan>
      </MainTitleContainer>
      <MainArticleContainer>
        <MainArticle
          onClick={() => {
            setMode(RESULT);
          }}
        >
          결과 보기
        </MainArticle>
      </MainArticleContainer>
    </>
  );
};

export default Finish;

Finish.propTypes = {
  setMode: PropTypes.func.isRequired,
};

recommendationMode/Result.js

체크한 문항에 따른 자취방 추천 결과를 볼 수 있다.

  • 카카오맵api를 통해 상위 5개 자취지역을 마커로 띄우고, 5개 지역 평균과 해당 마커의 차이를 우측 레이더차트에서 비교할 수 있다.

  • 하단에는 선택된 마커에 포함된 자취방들의 월세/전세 별 보증금, 세 차이와 핵심 키워드, 방 종류별 분포를 확인할 수 있다.

  • 하단에 있는 차트를 클릭하면 보다 자세한 통계와, 해당 자취방(원룸, 투룸 등)이 정확히 지도상의 어디에 분포하는 지 확인 가능하다.




import Loader from "../Loader";
import {
  BarChartSelect,
  BarChartSelectContainer,
  ResultArticleContainer,
  ResultCell,
  ResultDetailChartContainer,
  ResultDetailContainer,
  ResultDetailContentContainer,
  ResultDetailImg,
  ResultDetailImgContainer,
  ResultDetailSpan,
  ResultDetailSpanContainer,
  ResultMainContainer,
  ResultRow,
  ResultSubContainer,
  ResultSubTitleSpan,
  ResultTable,
  ResultTitleContainer,
  ResultTitleSpan,
} from "../styles/StyledComponents";
import Map from "../kakao/Map";
import Map2 from "../kakao/Map2";
import RadarArticle from "../Visualization/RadarArticle";
import BarComponent from "../Visualization/BarRoom";
import PieComponent from "../Visualization/PieRoom";
import WordcloudDetailItem from "../Visualization/Detail/WordcloudDetailItem";
import {
  ALL,
  BAR,
  MONTHPAY,
  MONTHRESERV,
  PIE,
  RESERV,
  WORDCLOUD,
} from "../Enum";
import PropTypes from "prop-types";
const Result = ({
  answers,
  data,
  house,
  setHouse,
  setCurrentAddress,
  setIsHovered,
  setIsClicked,
  aggregated,
  isHovered,
  isClicked,
  isChecked,
  chartData,
  currentAddress,
  chartmode,
  setChartmode,
  setIsChecked,
  unitTransformer,
  positions,
}) => {
  return (
    <>
      {data.length === 0 ? (
        <Loader />
      ) : (
        <>
          <ResultArticleContainer>
            <ResultTitleContainer>
              <ResultTitleSpan>
                "{answers.Q1Answer}" 주변 추천 자취지역 Top5
              </ResultTitleSpan>
              <ResultSubTitleSpan>
                마커에 마우스(손가락)() 올리시면 해당 지역과 평균을 비교할
                수 있습니다.
              </ResultSubTitleSpan>
            </ResultTitleContainer>
            <ResultMainContainer>
              <ResultSubContainer>
                <ResultSubTitleSpan>
                  마커를 클릭하면 해당 지역의 상세정보를 확인할 수 있습니다.
                </ResultSubTitleSpan>
                {data.length !== 0 && (
                  <Map
                    mobile={window.innerWidth <= 500}
                    setHouse={setHouse}
                    setCurrentAddress={setCurrentAddress}
                    setIsHovered={setIsHovered}
                    setIsClicked={setIsClicked}
                    data={data}
                    univ_lat={answers.univ_lat}
                    univ_lon={answers.univ_lon}
                  />
                )}
              </ResultSubContainer>
              <ResultSubContainer width={"40%"}>
                <ResultSubTitleSpan>
                  선택된 지역과 5개 지역 평균의 차이입니다.
                </ResultSubTitleSpan>
                {aggregated.length !== 0 && (
                  <RadarArticle
                    mobile={window.innerWidth <= 500}
                    data={aggregated}
                    isHovered={isHovered}
                    isClicked={isClicked}
                  />
                )}
              </ResultSubContainer>
            </ResultMainContainer>
          </ResultArticleContainer>
          {isClicked && chartData.hashtagsTotal.length !== 0 && (
            <ResultArticleContainer>
              <ResultTitleContainer>
                <ResultTitleSpan>
                  "{isClicked.rank}지역(
                  {currentAddress ? `${currentAddress}` : ``})" 주변 매물 관련
                  통계
                </ResultTitleSpan>
                <ResultSubTitleSpan>
                  {chartmode === ALL
                    ? "차트를 클릭하면 자세한 정보를 볼 수 있습니다."
                    : "차트를 클릭하면 이전 화면으로 돌아갈 수 있습니다."}
                </ResultSubTitleSpan>
              </ResultTitleContainer>
              {chartmode === ALL && (
                <ResultMainContainer>
                  <ResultSubContainer>
                    <ResultSubTitleSpan>
                      {isClicked.rank}위 지역과 전체 평균 간{" "}
                      <strong>
                        {isChecked === MONTHRESERV
                          ? "월세 보증금"
                          : isChecked === MONTHPAY
                          ? "월세"
                          : "전세 보증금"}
                      </strong>{" "}
                      보증금 비교(단위: 만 원)
                    </ResultSubTitleSpan>
                    <BarChartSelectContainer>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(MONTHRESERV)}
                      >
                        월세 보증금
                      </BarChartSelect>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(MONTHPAY)}
                      >
                        월세
                      </BarChartSelect>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(RESERV)}
                      >
                        전세
                      </BarChartSelect>
                    </BarChartSelectContainer>

                    <BarComponent
                      isChecked={isChecked}
                      chartmode={chartmode}
                      setChartmode={setChartmode}
                      monthlyDepositEachAggregated={
                        chartData.monthlyDepositEachAggregated
                      }
                      monthlyPayEachAggregated={
                        chartData.monthlyPayEachAggregated
                      }
                      reservDepositEachAggregated={
                        chartData.reservDepositEachAggregated
                      }
                      monthlyDepositTotalAggregated={
                        chartData.monthlyDepositTotalAggregated
                      }
                      monthlyPayTotalAggregated={
                        chartData.monthlyPayTotalAggregated
                      }
                      reservDepositTotalAggregated={
                        chartData.reservDepositTotalAggregated
                      }
                      clickedMarker={isClicked.rank - 1}
                    />
                  </ResultSubContainer>
                  <ResultSubContainer>
                    <ResultSubTitleSpan>
                      {isClicked.rank}위 지역의 핵심 키워드
                    </ResultSubTitleSpan>
                    <BarChartSelectContainer></BarChartSelectContainer>
                    <WordcloudDetailItem
                      mobile={window.innerWidth <= 500}
                      hashtags={chartData.hashtagsEach[isClicked.rank - 1]}
                      chartmode={chartmode}
                      setChartmode={setChartmode}
                    />
                  </ResultSubContainer>
                  <ResultSubContainer>
                    <ResultSubTitleSpan>
                      {isClicked.rank}위 지역의 매물 종류 분포
                    </ResultSubTitleSpan>
                    <BarChartSelectContainer></BarChartSelectContainer>
                    <PieComponent
                      isClicked={isClicked}
                      chartmode={chartmode}
                      setChartmode={setChartmode}
                    />
                  </ResultSubContainer>
                </ResultMainContainer>
              )}
              {chartmode === BAR && (
                <ResultDetailContainer>
                  <ResultDetailChartContainer>
                    <ResultSubTitleSpan>
                      {isClicked.rank}위 지역과 전체 평균 간{" "}
                      <strong>
                        {isChecked === MONTHRESERV
                          ? "월세 보증금"
                          : isChecked === MONTHPAY
                          ? "월세"
                          : "전세 보증금"}
                      </strong>{" "}
                      보증금 비교(단위: 만 원)
                    </ResultSubTitleSpan>
                    <BarChartSelectContainer>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(MONTHRESERV)}
                      >
                        월세 보증금
                      </BarChartSelect>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(MONTHPAY)}
                      >
                        월세
                      </BarChartSelect>
                      <BarChartSelect
                        isChecked={isChecked}
                        onClick={() => setIsChecked(RESERV)}
                      >
                        전세
                      </BarChartSelect>
                    </BarChartSelectContainer>
                    <BarComponent
                      isChecked={isChecked}
                      chartmode={chartmode}
                      setChartmode={setChartmode}
                      monthlyDepositEachAggregated={
                        chartData.monthlyDepositEachAggregated
                      }
                      monthlyPayEachAggregated={
                        chartData.monthlyPayEachAggregated
                      }
                      reservDepositEachAggregated={
                        chartData.reservDepositEachAggregated
                      }
                      monthlyDepositTotalAggregated={
                        chartData.monthlyDepositTotalAggregated
                      }
                      monthlyPayTotalAggregated={
                        chartData.monthlyPayTotalAggregated
                      }
                      reservDepositTotalAggregated={
                        chartData.reservDepositTotalAggregated
                      }
                      clickedMarker={isClicked.rank - 1}
                    />
                  </ResultDetailChartContainer>
                  <ResultDetailContentContainer>
                    {isClicked.rank}위 지역의 통계
                    <ResultTable>
                      <ResultRow>
                        <ResultCell></ResultCell>
                        <ResultCell>최고가</ResultCell>
                        <ResultCell>최저가</ResultCell>
                        <ResultCell>평균가</ResultCell>
                        <ResultCell>매물 수</ResultCell>
                      </ResultRow>
                      <ResultRow>
                        <ResultCell>월세 보증금</ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyDepositEachAggregated.max[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyDepositEachAggregated.min[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyDepositEachAggregated.avg[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {
                            chartData.monthlyDepositEachAggregated.count[
                              isClicked.rank - 1
                            ]
                          }</ResultCell>
                      </ResultRow>
                      <ResultRow>
                        <ResultCell>월세</ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyPayEachAggregated.max[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyPayEachAggregated.min[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.monthlyPayEachAggregated.avg[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>''</ResultCell>
                      </ResultRow>
                      <ResultRow>
                        <ResultCell>전세 보증금</ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.reservDepositEachAggregated.max[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.reservDepositEachAggregated.min[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {unitTransformer(
                            chartData.reservDepositEachAggregated.avg[
                              isClicked.rank - 1
                            ]
                          )}
                        </ResultCell>
                        <ResultCell>
                          {
                            chartData.reservDepositEachAggregated.count[
                              isClicked.rank - 1
                            ]
                          }</ResultCell>
                      </ResultRow>
                    </ResultTable>
                    <ResultDetailSpanContainer>
                      <ResultDetailSpan>
                        {isClicked.rank}위 지역은 <strong>월세 보증금</strong>이
                        평균에 비해{" "}
                        <strong>
                          {Math.abs(
                            chartData.monthlyDepositEachAggregated.avg[
                              isClicked.rank - 1
                            ] - chartData.monthlyDepositTotalAggregated.avg
                          )}
                          만 원{" "}
                        </strong>
                        {chartData.monthlyDepositEachAggregated.avg[
                          isClicked.rank - 1
                        ] -
                          chartData.monthlyDepositTotalAggregated.avg >=
                        0
                          ? "비싸네요."
                          : "싸네요."}
                      </ResultDetailSpan>
                      <ResultDetailSpan>
                        {isClicked.rank}위 지역은 <strong>월세</strong>가 평균에
                        비해{" "}
                        <strong>
                          {Math.abs(
                            chartData.monthlyPayEachAggregated.avg[
                              isClicked.rank - 1
                            ] - chartData.monthlyPayTotalAggregated.avg
                          )}
                          만 원{" "}
                        </strong>
                        {chartData.monthlyPayEachAggregated.avg[
                          isClicked.rank - 1
                        ] -
                          chartData.monthlyPayTotalAggregated.avg >=
                        0
                          ? "비싸네요."
                          : "싸네요."}
                      </ResultDetailSpan>
                      <ResultDetailSpan>
                        {isClicked.rank}위 지역은 <strong>전세 보증금</strong>이
                        평균에 비해{" "}
                        <strong>
                          {Math.abs(
                            chartData.reservDepositEachAggregated.avg[
                              isClicked.rank - 1
                            ] - chartData.reservDepositTotalAggregated.avg
                          )}
                          만 원{" "}
                        </strong>
                        {chartData.reservDepositEachAggregated.avg[
                          isClicked.rank - 1
                        ] -
                          chartData.reservDepositTotalAggregated.avg >=
                        0
                          ? "비싸네요."
                          : "싸네요."}
                      </ResultDetailSpan>
                      <ResultDetailSpan>
                        그리고,{" "}
                        <strong>
                          {chartData.reservDepositEachAggregated.count[
                            isClicked.rank - 1
                          ] +
                            chartData.monthlyDepositEachAggregated.count[
                              isClicked.rank - 1
                            ]}
                          개의 전세/월세 매물
                        </strong>
                        이 있군요.
                      </ResultDetailSpan>
                    </ResultDetailSpanContainer>
                  </ResultDetailContentContainer>
                </ResultDetailContainer>
              )}
              {chartmode === WORDCLOUD && (
                <ResultDetailContainer>
                  <ResultDetailChartContainer>
                    <ResultSubTitleSpan>
                      {isClicked.rank}위 지역의 핵심 키워드
                    </ResultSubTitleSpan>
                    <BarChartSelectContainer></BarChartSelectContainer>
                    <WordcloudDetailItem
                      mobile={window.innerWidth <= 500}
                      hashtags={chartData.hashtagsEach[isClicked.rank - 1]}
                      chartmode={chartmode}
                      setChartmode={setChartmode}
                    />
                  </ResultDetailChartContainer>
                </ResultDetailContainer>
              )}
              {chartmode === PIE && (
                <ResultDetailContainer>
                  {house ? (
                    <ResultDetailChartContainer>
                      <ResultSubTitleSpan>선택된 매물 정보</ResultSubTitleSpan>
                      <ResultDetailImgContainer>
                        <ResultDetailImg
                          onClick={() => setHouse()}
                          src={isClicked.rooms_img_url_01[house]}
                          alt="매물 사진"
                        />
                      </ResultDetailImgContainer>
                      <ResultDetailSpan>
                        {isClicked.rooms_desc[house]}
                      </ResultDetailSpan>
                      <ResultDetailSpan>
                        {isClicked.rooms_desc2[house]}
                      </ResultDetailSpan>
                      <ResultDetailSpan>
                        {isClicked.rooms_price_title[house]}
                      </ResultDetailSpan>
                    </ResultDetailChartContainer>
                  ) : (
                    <ResultDetailChartContainer>
                      <ResultSubTitleSpan>
                        {isClicked.rank}위 지역의 매물 종류 분포
                      </ResultSubTitleSpan>
                      <PieComponent
                        isClicked={isClicked}
                        chartmode={chartmode}
                        setChartmode={setChartmode}
                      />
                    </ResultDetailChartContainer>
                  )}
                  <ResultDetailContentContainer>
                    {positions.length !== 0 && (
                      <>
                        <ResultSubTitleSpan>
                          {isClicked.rank}위 지역의 매물 분포도
                        </ResultSubTitleSpan>
                        <Map2
                          isClicked={isClicked}
                          univ_lat={answers.univ_lat}
                          univ_lon={answers.univ_lon}
                          residencePositions={positions}
                          setHouse={setHouse}
                          mobile={false}
                        />
                      </>
                    )}
                  </ResultDetailContentContainer>
                </ResultDetailContainer>
              )}
            </ResultArticleContainer>
          )}
        </>
      )}
    </>
  );
};

export default Result;

Result.propTypes = {
  answers: PropTypes.object.isRequired,
  data: PropTypes.array.isRequired,
  house: PropTypes.object,
  setHouse: PropTypes.func.isRequired,
  setCurrentAddress: PropTypes.func.isRequired,
  setIsHovered: PropTypes.func.isRequired,
  setIsClicked: PropTypes.func.isRequired,
  aggregated: PropTypes.array.isRequired,
  isHovered: PropTypes.string.isRequired,
  isClicked: PropTypes.string.isRequired,
  isChecked: PropTypes.string.isRequired,
  chartData: PropTypes.object.isRequired,
  currentAddress: PropTypes.string.isRequired,
  chartmode: PropTypes.string.isRequired,
  setChartmode: PropTypes.func.isRequired,
  setIsChecked: PropTypes.func.isRequired,
  unitTransformer: PropTypes.func.isRequired,
  positions: PropTypes.array.isRequired,
};

ArticleButton.js

Q3,Q4,Q5에서 버튼 부분의 컴포넌트이다.

  • 클릭 여부에 따라 style과 png 파일이 달라지도록 hook으로 관리했다.

import { useEffect, useState } from "react";
import styled from "styled-components";
import { Q3, Q4, Q5 } from "./Enum";
import PropTypes from "prop-types";
const MainArticle = styled.div`
  width: 12vw;
  height: 12vw;
  display: ${(props) => (props.display ? "flex" : "none")};
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border: ${(props) =>
    props.isSelected ? `4px solid #f7323f` : `4px solid rgba(0, 0, 0, 0.5)`};
  border-radius: 10%;
  font-size: 1.4vw;
  font-weight: 600;
  color: ${(props) => (props.isSelected ? `#f7323f` : `black`)};
  cursor: pointer;
  :hover {
    border: 4px solid #f7323f;
    color: #f7323f;
  }
`;
const MainArticleButtonImg = styled.img`
  height: 60%;
  width: auto;
  margin-bottom: 1vw;
`;

const ArticleButton = ({
  current,
  name,
  kr_name,
  black_img,
  red_img,
  answers,
  setAnswers,
  isSelected,
}) => {
  const [buttonOnMouse, setButtonOnMouse] = useState(false);
  const [display, setDisplay] = useState(true);
  useEffect(() => {
    //현재 이 버튼이 몇 단계에 쓰이는 요소의 버튼인지 확인하고,
    //이전 단계에서 해당 버튼이 선택되었다면, 다음 단계에선 안보이게한다.
    if (current === Q4) {
      setDisplay(answers.Q3Answer !== name);
    } else if (current === Q5) {
      setDisplay(answers.Q3Answer !== name && answers.Q4Answer !== name);
    }
  }, [current, answers.Q3Answer, answers.Q4Answer, name]);
  return (
    <MainArticle
      display={display}
      isSelected={isSelected}
      onClick={() => {
        //이 버튼이 몇 단계에 쓰이는 요소의 버튼인지 확인하고,
        //눌린 버튼을 각 단계의 Answer로 할당한다.
        if (current === Q3) {
          setAnswers({ ...answers, Q3Answer: name, Q3Answer_kr: kr_name });
        } else if (current === Q4) {
          setAnswers({ ...answers, Q4Answer: name, Q4Answer_kr: kr_name });
        } else {
          setAnswers({ ...answers, Q5Answer: name, Q5Answer_kr: kr_name });
        }
      }}
      //버튼 hover 스타일을 위한 코드
      onMouseLeave={() => setButtonOnMouse(false)}
      onMouseEnter={() => setButtonOnMouse(true)}
    >
      //버튼 위에 커서가 있거나, 선택되었다면 빨간색으로 바꾼다.
      <MainArticleButtonImg
        src={buttonOnMouse || isSelected ? red_img : black_img}
      />
      {kr_name}
    </MainArticle>
  );
};

export default ArticleButton;

ArticleButton.propTypes = {
  current: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  kr_name: PropTypes.string.isRequired,
  black_img: PropTypes.string.isRequired,
  red_img: PropTypes.string.isRequired,
  answers: PropTypes.object.isRequired,
  setAnswers: PropTypes.func.isRequired,
  isSelected: PropTypes.bool.isRequired,
};

RouterComponents.js

react의 라우팅을 위해, 해당하는 url에 맞는 screen 컴포넌트를 노출시키기 위한 코드

  • 마지막의 *는 그 외 다른 url을 NotFound로 보내는 코드이다.

import React from "react";
import { Route, Routes } from "react-router-dom";
import Aboutus from "../screens/Aboutus";
import Recommendation from "../screens/Recommendation";
import Lab from "../screens/Lab";
import History from "../screens/History";
import Home from "../screens/Home";

const RouterComponent = () => {
  return (
    <Routes>
    //exact는 정확하게 path와 일치해야지만 라우팅시키겠다는 props다.
      <Route exact path="/" element={<Home />} />
      <Route exact path="/Aboutus" element={<Aboutus />} />
      <Route exact path="/Recommendation" element={<Recommendation />} />
      <Route exact path="/Lab" element={<Lab />} />
      <Route exact path="/History" element={<History />} />
      <Route exact path="*" element={<h1>NOT FOUND</h1>} />
    </Routes>
  );
};

export default RouterComponent;

RecommendationContainer.js

이 프로젝트의 핵심 screen페이지인 추천의 처음과 결과창까지 품고 있는 컴포넌트이다.

  • 추천 페이지에 쓰이는 모든 hook과 함수가 들어있다.

import React, { useEffect, useRef, useState } from "react";
import { FINISH, INTRO, ALL, MONTHRESERV } from "../../components/Enum";
import useInput from "../../components/hooks/useInput";
import { Api } from "../../api";
import RecommendationPresenter from "./RecommendationPresenter";
const { kakao } = window;

const RecommendationContainer = () => {
  //현재 모드를 설정. INTRO, Q1~Q5, FINISH, RESULT 중
  const [mode, setMode] = useState(INTRO);
  //Q1에서 학교 이름을 검색할 때 쓰이는 hook
  const schoolNameInput = useInput("");
  //Q1에서 학교 이름을 검색하는 input에 쓰일 ref hook
  const schoolNameInputRef = useRef();
  //Q1~Q5 단계에서 선택된 항목들을 관리하는 hook
  const [answers, setAnswers] = useState({
    Q1Answer: "",
    univ_lat: "",
    univ_lon: "",
    Q2Answer: "",
    Q3Answer: "",
    Q4Answer: "",
    Q5Answer: "",
    Q3Answer_kr: "",
    Q4Answer_kr: "",
    Q5Answer_kr: "",
  });
  //위에서 입력된 answers를 이용하여 fetch된 데이터를 저장한다.
  const [data, setData] = useState([]);
  //레이더차트에 쓰일 가중치의 평균값과 개별값들을 저장한다.
  const [aggregated, setAggregated] = useState([]);
  //RESULT페이지에서 선택된 마커의 위도,경도 값을 변환하여 동주소(ex.서울시 서대문구 북가좌동)을 담는다
  const [currentAddress, setCurrentAddress] = useState("");
  //파이차트 세부페이지(지도에 방 분포를 보여주는 페이지)에서 각 실제 자취방의 정보를 담음
  const [house, setHouse] = useState();
  //카카오맵api에서 선택된 마커의 정보를 담는 hook
  const [isClicked, setIsClicked] = useState("");
  //카카오맵api에서 hovering된 마커의 정보를 담는 hook
  const [isHovered, setIsHovered] = useState("");
  //선택된 마커의 정보와 5개 지역의 마커의 모든 정보를 이용하여,
  //차트에 사용하기 쉬운 형태로 가공
  const [chartData, setChartData] = useState({
    //마커 각각의 해시태그
    hashtagsEach: [],
    //마커 각각의 월세보증금의 최대,최소,평균,월세방의 개수를 담음
    monthlyDepositEachAggregated: {},
    //마커 각각의 월세금의 최대,최소,평균,월세방의 개수를 담음
    monthlyPayEachAggregated: {},
    //마커 각각의 전세보증금의 최대,최소,평균,전세방의 개수를 담음
    reservDepositEachAggregated: {},
    //마커 5개에 포함된 모든 해시태그
    hashtagsTotal: [],
    //마커 5개 전체의 월세보증금의 최대,최소,평균,월세방의 개수를 담음
    monthlyDepositTotalAggregated: {},
    //마커 5개 전체의 월세금의 최대,최소,평균,월세방의 개수를 담음
    monthlyPayTotalAggregated: {},
    //마커 5개 전체의 전세보증금의 최대,최소,평균,전세방의 개수를 담음
    reservDepositTotalAggregated: {},
  });
  //RESULT mode의 두번째 article에서 BarChart의 차트 카테고리를 담음
  //MONTHRESERV는 월세보증금을 보여주는 막대그래프를 보여줌
  //MONTHPAY는 월세금을 보여주는 막대그래프를 보여줌
  //RESERV는 전세보증금을 보여주는 막대그래프를 보여줌
  const [isChecked, setIsChecked] = useState(MONTHRESERV);
  //RESULT mode의 두번째 article의 화면 모드 담당
  //ALL이면 3개 차트를 작게 셋 다 보여줌
  //BAR이면 bar차트와 통계자료 노출
  //PIE이면 pie차트와 매물 실제 위치 지도에 노출 및 대략적인 정보제공
  //WORDCLOUD이면 워드클라우드 크게 표시
  const [chartmode, setChartmode] = useState(ALL);
  //PIE모드에서 모든 매물들의 실제 위치를 카카오맵api 지도에 표시하기 위해 필요한 위치배열.
  const [positions, setPositions] = useState([]);

  
  //레이더차트 라이브러리에 맞는 형태로 가공하는 함수
  const getAggregated = () => {
    //마커 레이더차트
    let weight_names = ["거리", "역세권", "가성비", "안전", "매물"];
    let newArr = [];
    for (let i = 0; i < data.length; i++) {
      let tempObj = {};
      tempObj["weight"] = weight_names[i];
      for (let j = 0; j < 5; j++) {
        tempObj[`${j + 1}`] = data[j][`T${i + 1}`];
      }
      tempObj[`평균`] = Math.round(data[0][`T${i + 1}_avg`]);
      newArr.push(tempObj);
    }
    console.log(newArr);
    setAggregated(newArr);
  };

  //chartData를 얻기위해 데이터를 가공하는 함수
  const getChartAggregated = () => {
    //해시태그 모음
    const hashtagsEach = [];
    const hashtagsTotalTemp = [];
    const hashtagsTotal = [];

    //월세보증금
    const monthlyDepositEachAggregated = {
      max: [],
      min: [],
      avg: [],
      count: [],
    };
    const monthlyDepositTotalTemp = [];
    const monthlyDepositTotalAggregated = {
      max: [],
      min: [],
      avg: [],
      count: [],
    };

    //월세
    const monthlyPayEachAggregated = { max: [], min: [], avg: [], count: [] };
    const monthlyPayTotalTemp = [];
    const monthlyPayTotalAggregated = { max: [], min: [], avg: [], count: [] };

    //전세
    const reservDepositEachAggregated = {
      max: [],
      min: [],
      avg: [],
      count: [],
    };
    const reservDepositTotalTemp = [];
    const reservDepositTotalAggregated = {
      max: [],
      min: [],
      avg: [],
      count: [],
    };

    for (let i = 0; i < data.length; i++) {
      const hashtagsTemp = [];
      const monthlyDepositTemp = [];
      const monthlyPayTemp = [];
      const reservDepositTemp = [];
      const priceTemp = [];
    
      for (let j = 0; j < data[i].rooms_hash_tags.length; j++) {
        hashtagsTemp.push(data[i].rooms_hash_tags[j]);
      }

      for (let j = 0; j < data[i].rooms_price_title.length; j++) {
        hashtagsTemp.push(data[i].rooms_desc[j].split("|")[0].trim());
        hashtagsTemp.push(data[i].rooms_desc2[j].split(",")[0].trim());

        //가격에 '억'이라는 글자를 단위 만 원으로 환산하기 위해, 10000을 곱해서 단위를 통일시켜주는 작업
        if (data[i].rooms_selling_type[j] === 0) {
          if (data[i].rooms_price_title[j].split("/")[0].includes("억")) {
            monthlyDepositTemp.push(
              Number(
                data[i].rooms_price_title[j].split("/")[0].split("억")[0]
              ) *
                10000 +
                Number(
                  data[i].rooms_price_title[j].split("/")[0].split("억")[1]
                )
            );
          } else {
            monthlyDepositTemp.push(
              Number(data[i].rooms_price_title[j].split("/")[0])
            );
          }
          if (data[i].rooms_price_title[j].split("/")[1].includes("억")) {
            monthlyPayTemp.push(
              Number(
                data[i].rooms_price_title[j].split("/")[1].split("억")[0]
              ) *
                10000 +
                Number(
                  data[i].rooms_price_title[j].split("/")[1].split("억")[1]
                )
            );
          } else {
            monthlyPayTemp.push(
              Number(data[i].rooms_price_title[j].split("/")[1])
            );
          }
        } else if (data[i].rooms_selling_type[j] === 1) {
          if (data[i].rooms_price_title[j].includes("억")) {
            reservDepositTemp.push(
              Number(data[i].rooms_price_title[j].split("억")[0]) * 10000 +
                Number(data[i].rooms_price_title[j].split("억")[1])
            );
          } else {
            reservDepositTemp.push(Number(data[i].rooms_price_title[j]));
          }
        } else {
          if (data[i].rooms_price_title[j].includes("억")) {
            priceTemp.push(
              Number(data[i].rooms_price_title[j].split("억")[0]) * 10000 +
                Number(data[i].rooms_price_title[j].split("억")[1])
            );
          } else {
            priceTemp.push(Number(data[i].rooms_price_title[j]));
          }
        }
      }
      hashtagsEach.push(hashtagsTemp);

      //최대,최소,평균값과 개수를 구하는 코드
      monthlyDepositEachAggregated.max.push(Math.max(...monthlyDepositTemp));
      monthlyPayEachAggregated.max.push(Math.max(...monthlyPayTemp));
      reservDepositEachAggregated.max.push(Math.max(...reservDepositTemp));

      monthlyDepositEachAggregated.min.push(Math.min(...monthlyDepositTemp));
      monthlyPayEachAggregated.min.push(Math.min(...monthlyPayTemp));
      reservDepositEachAggregated.min.push(Math.min(...reservDepositTemp));

      monthlyDepositEachAggregated.avg.push(
        Math.round(
          monthlyDepositTemp.reduce((a, b) => a + b, 0) /
            monthlyDepositTemp.length
        )
      );
      monthlyPayEachAggregated.avg.push(
        Math.round(
          monthlyPayTemp.reduce((a, b) => a + b, 0) / monthlyPayTemp.length
        )
      );
      reservDepositEachAggregated.avg.push(
        Math.round(
          reservDepositTemp.reduce((a, b) => a + b, 0) /
            reservDepositTemp.length
        )
      );

      monthlyDepositEachAggregated.count.push(monthlyDepositTemp.length);
      monthlyPayEachAggregated.count.push(monthlyPayTemp.length);
      reservDepositEachAggregated.count.push(reservDepositTemp.length);

      hashtagsTotalTemp.push(...hashtagsTemp);
      monthlyDepositTotalTemp.push(...monthlyDepositTemp);
      monthlyPayTotalTemp.push(...monthlyPayTemp);
      reservDepositTotalTemp.push(...reservDepositTemp);
    }

    hashtagsTotal.push(...hashtagsTotalTemp);

    monthlyDepositTotalAggregated.max.push(
      Math.max(...monthlyDepositTotalTemp)
    );
    monthlyPayTotalAggregated.max.push(Math.max(...monthlyPayTotalTemp));
    reservDepositTotalAggregated.max.push(Math.max(...reservDepositTotalTemp));

    monthlyDepositTotalAggregated.min.push(
      Math.min(...monthlyDepositTotalTemp)
    );
    monthlyPayTotalAggregated.min.push(Math.min(...monthlyPayTotalTemp));
    reservDepositTotalAggregated.min.push(Math.min(...reservDepositTotalTemp));

    monthlyDepositTotalAggregated.avg.push(
      Math.round(
        monthlyDepositTotalTemp.reduce((a, b) => a + b, 0) /
          monthlyDepositTotalTemp.length
      )
    );
    monthlyPayTotalAggregated.avg.push(
      Math.round(
        monthlyPayTotalTemp.reduce((a, b) => a + b, 0) /
          monthlyPayTotalTemp.length
      )
    );
    reservDepositTotalAggregated.avg.push(
      Math.round(
        reservDepositTotalTemp.reduce((a, b) => a + b, 0) /
          reservDepositTotalTemp.length
      )
    );

    monthlyDepositTotalAggregated.count.push(monthlyDepositTotalTemp.length);
    monthlyPayTotalAggregated.count.push(monthlyPayTotalTemp.length);
    reservDepositTotalAggregated.count.push(reservDepositTotalTemp.length);

    setChartData({
      hashtagsEach,
      monthlyDepositEachAggregated,
      monthlyPayEachAggregated,
      reservDepositEachAggregated,
      hashtagsTotal,
      monthlyDepositTotalAggregated,
      monthlyPayTotalAggregated,
      reservDepositTotalAggregated,
    });

    console.log(chartData);
  };

  //RESULT mode에서 chartmode===BAR일 때, 전월세보증금의 통계자료를 보여주는 페이지에서, 가격을 명시하는 부분을 함수화함
  const unitTransformer = (value) => {
    return value >= 10000
      ? `${Math.floor(value / 10000)}${
          value % 10000 === 0 ? "(원)" : `${value % 10000}(만 원)`
        }`
      : `${value}(만 원)`;
  };

  //RESULT mode에서 chartmode===PIE일 때, 매물들의 실제 위치를 카카오맵api의 지도에 뿌려주기 위해 실제 방의 위치를 position hook에 저장함
  const getPositions = () => {
    console.log(isClicked.rooms_location_lat);
    if (isClicked) {
      let temp = [];
      for (let i = 0; i < isClicked.rooms_location_lat.length; i++) {
        temp.push({
          latlng: new kakao.maps.LatLng(
            isClicked.rooms_location_lat[i],
            isClicked.rooms_location_lon[i]
          ),
        });
      }
      setPositions(temp);
    }
  };
  useEffect(() => {
    //모든 Answer의 유효성 검사가 끝나고, 현재 모드가 FINISH라면 fetch함
    if (
      answers.Q1Answer !== "" &&
      answers.Q2Answer !== "" &&
      answers.Q3Answer !== "" &&
      answers.Q4Answer !== "" &&
      answers.Q5Answer !== "" &&
      answers.univ_lat !== "" &&
      answers.univ_lon !== "" &&
      mode === FINISH
    ) {
      console.log(answers);
      Api.getResidence(answers).then((res) => {
        console.log(res.data);
        //백엔드에서 보낸 success가 true라면 성공이므로
        if (res.data.success) {
          //json으로 파싱하기 위해 작은따옴표를 큰따옴표로 바꿈(안바꾸면 에러생김)
          let parsed = res.data.data.replaceAll("'", '"');
          parsed = JSON.parse(parsed);
          //(디버깅할 때 안지우고 남겨뒀던 코드)
          console.log(parsed);
          console.log(typeof parsed);
          setData(parsed);
          //로그 용도로 바로 추천 이력을 DB에 저장함
          if (parsed) {
            Api.saveResult(
              answers.Q1Answer,
              answers.univ_lat,
              answers.univ_lon,
             //정규화에 어긋나지만 일단 이렇게 저장함
              `[${parsed[0].code},${parsed[1].code},${parsed[2].code},${parsed[3].code},${parsed[4].code}]`,
              `[${parsed[0].T1},${parsed[0].T2},${parsed[0].T3},${parsed[0].T4},${parsed[0].T5}]`,

              `[${parsed[1].T1},${parsed[1].T2},${parsed[1].T3},${parsed[1].T4},${parsed[1].T5}]`,

              `[${parsed[2].T1},${parsed[2].T2},${parsed[2].T3},${parsed[2].T4},${parsed[2].T5}]`,

              `[${parsed[3].T1},${parsed[3].T2},${parsed[3].T3},${parsed[3].T4},${parsed[3].T5}]`,

              `[${parsed[4].T1},${parsed[4].T2},${parsed[4].T3},${parsed[4].T4},${parsed[4].T5}]`,

              `[${Math.round(parsed[0].T1_avg)},${Math.round(
                parsed[0].T2_avg
              )},${Math.round(parsed[0].T3_avg)},${Math.round(
                parsed[0].T4_avg
              )},${Math.round(parsed[0].T5_avg)}]`
            ).then((res) => {
              console.log(res.data);
            });
          }
        } else {
          //DB저장에 실패했다면 백엔드에서 보낸 에러메시지 alert로 노출
          alert(res.data.err_msg);
        }
      });
    }
    //데이터가 있고, 아직 aggregated에 데이터가 할당되지 않았다면,
    if (data.length !== 0 && aggregated.length === 0) {
      getAggregated();
    }
    //데이터가 있고, 리랜더링 되었을 때,
    if (data.length !== 0) {
      getChartAggregated();
    }
    //데이터가 있고, 리랜더링 되었을 때,
    if (data.length !== 0) {
      getPositions();
    }
    console.log(isClicked);
    //mode와 isHovered와 isClicked가 바뀔때마다 리랜더링됨.
  }, [mode, isHovered, isClicked]);
  return (
    <RecommendationPresenter
      mode={mode}
      answers={answers}
      setMode={setMode}
      setAnswers={setAnswers}
      schoolNameInputRef={schoolNameInputRef}
      schoolNameInput={schoolNameInput}
      data={data}
      house={house}
      setHouse={setHouse}
      setCurrentAddress={setCurrentAddress}
      setIsHovered={setIsHovered}
      setIsClicked={setIsClicked}
      aggregated={aggregated}
      isHovered={isHovered}
      isClicked={isClicked}
      isChecked={isChecked}
      chartData={chartData}
      currentAddress={currentAddress}
      chartmode={chartmode}
      setChartmode={setChartmode}
      setIsChecked={setIsChecked}
      unitTransformer={unitTransformer}
      positions={positions}
    />
  );
};
export default RecommendationContainer;

RecommendationPresenter.js

RecommendationContainer.js 로직으로 처리된 props들을 스타일링해서 뿌려주는 Presenter이다.

  • 모든 스타일컴포넌트는 StyledComponents.js에 export문으로 모아두었다.

import Intro from "../../components/recommendationMode/Intro";
import Q1Component from "../../components/recommendationMode/Q1";
import Q2Component from "../../components/recommendationMode/Q2";
import Q3Component from "../../components/recommendationMode/Q3";
import Q4Component from "../../components/recommendationMode/Q4";
import Q5Component from "../../components/recommendationMode/Q5";
import Finish from "../../components/recommendationMode/Finish";
import Result from "../../components/recommendationMode/Result";
import {
  FINISH,
  INTRO,
  Q1,
  Q2,
  Q3,
  Q4,
  Q5,
  RESULT,
} from "../../components/Enum";
import {
  MainContainer,
  SelectedContainer,
  SelectedSpan,
} from "../../components/styles/StyledComponents";
import Helmet from "react-helmet";
const RecommendationPresenter = ({
  mode,
  answers,
  setMode,
  setAnswers,
  schoolNameInputRef,
  schoolNameInput,
  data,
  house,
  setHouse,
  setCurrentAddress,
  setIsHovered,
  setIsClicked,
  aggregated,
  isHovered,
  isClicked,
  isChecked,
  chartData,
  currentAddress,
  chartmode,
  setChartmode,
  setIsChecked,
  unitTransformer,
  positions,
}) => {
  return (
    <>
      <Helmet>
        <title>Unibangcity | Recommendation</title>
      </Helmet>
      <MainContainer mode={mode}>
        {mode !== RESULT && (
          <SelectedContainer>
            <SelectedSpan>{answers.Q1Answer}</SelectedSpan>+
            <SelectedSpan>
              {answers.Q2Answer ? `${answers.Q2Answer}m` : ``}
            </SelectedSpan>
            +<SelectedSpan>{answers.Q3Answer_kr}</SelectedSpan>+
            <SelectedSpan>{answers.Q4Answer_kr}</SelectedSpan>+
            <SelectedSpan>{answers.Q5Answer_kr}</SelectedSpan>
          </SelectedContainer>
        )}
        {mode === INTRO && <Intro setMode={setMode} />}
        {mode === Q1 && (
          <Q1Component
            setMode={setMode}
            answers={answers}
            setAnswers={setAnswers}
            schoolNameInputRef={schoolNameInputRef}
            schoolNameInput={schoolNameInput}
          />
        )}
        {mode === Q2 && (
          <Q2Component
            answers={answers}
            setAnswers={setAnswers}
            setMode={setMode}
          />
        )}
        {mode === Q3 && (
          <Q3Component
            answers={answers}
            setAnswers={setAnswers}
            setMode={setMode}
          />
        )}
        {mode === Q4 && (
          <Q4Component
            answers={answers}
            setAnswers={setAnswers}
            setMode={setMode}
          />
        )}
        {mode === Q5 && (
          <Q5Component
            answers={answers}
            setAnswers={setAnswers}
            setMode={setMode}
          />
        )}
        {mode === FINISH && <Finish setMode={setMode} />}
        {mode === RESULT && (
          <Result
            answers={answers}
            data={data}
            house={house}
            setHouse={setHouse}
            setCurrentAddress={setCurrentAddress}
            setIsHovered={setIsHovered}
            setIsClicked={setIsClicked}
            aggregated={aggregated}
            isHovered={isHovered}
            isClicked={isClicked}
            isChecked={isChecked}
            chartData={chartData}
            currentAddress={currentAddress}
            chartmode={chartmode}
            setChartmode={setChartmode}
            setIsChecked={setIsChecked}
            unitTransformer={unitTransformer}
            positions={positions}
          />
        )}
      </MainContainer>
    </>
  );
};

export default RecommendationPresenter;
profile
코더가 아닌 프로그래머를 지향하는 개발자

0개의 댓글