원티드 X 코드스테이츠 프리온보딩 프론트엔드 과정 기업과제 6번

H Kim·2022년 3월 15일
0

기업과제

목록 보기
7/8
post-thumbnail

원티드 X 코드스테이츠 프리온보딩 프론트엔드 과정 기업과제 6번

담당했던 부분 : Header, Footer 컴포넌트 작성 / 시작, 돌봄 유형 선택, 정보 확인, 마지막 페이지 작성


✨ 주요 기능

  • 간병인 신청하기를 모바일 웹 페이지로 구현하였습니다.
  • 케어코디 홈 화면에서 신청하기 버튼을 누르면 간병인을 신청하는 페이지로 넘어갈 수 있습니다.
  • 돌봄 유형을 24시간 상주(DAY)와 시간제 돌봄(TIME) 중에 선택할 수 있습니다.
  • 돌봄 스케줄을 달력 라이브러리를 사용하여 사용자가 원하는 날짜와 시간을 선택할 수 있습니다.
  • 돌봄 주소를 입력하는 페이지에서는 주소를 검색하면 주소 검색페이지로 넘어갑니다.
  • 주소 검색페이지는 페이지네이션으로 구현되어 있습니다.
    신청 완료 페이지에서는 사용자가 입력했던 정보를 다시 한 번 확인 할 수 있게 도와줍니다.
  • 전화번호를 입력하고 최종적으로 신청을 완료합니다.
    각 페이지에서는 옵션을 선택하지 않을 시 또는 정보를 모두 입력하지 않을 시 다음 버튼이 활성화 되지 않습니다.

이번 프로젝트에서는 재사용되는 컴포넌트들과 페이지들을 주로 작업하였다. 그 전의 프로젝트들에서는 재사용되는 컴포넌트들을 만들게 되어도 자꾸 재사용성을 생각하지 않고 그냥 만드는 무지성적인 일을 해서 팀원분들이 뚝딱뚝딱 도와주셨는데 이번에는 처음에 만들 때부터 "재사용한다...! 재사용한다...!" 이러면서 작업해서 재사용성에 맞게 작업할 수 있었다!

그리고 타입스크립트와 스타일드 컴포넌트를 같이 쓰면 타입스크립트가 너무 느려지는 사태가 자꾸 발생했어서 이번에는 좀 더 가볍다는 스타일드-jsx를 사용해서 작업해보았다. 사실 기본적으로는 스타일드 컴포넌트와 크게 차이가 없어서 새로 배운 게 있다고 하기도 약간 뭐 하기는 한데 그래도 새로 사용하면서 이것저것 찾아보기도 했는데 나름 재밌었다. 너무 스타일드 류만 쓰다보니 SCSS는 전~혀 사용해보지 않아서 선택자부터 어떻게 쓰는지를 전혀 모르게 되었기에 코스가 끝나고 혼자서 좀 사용해 보아야겠다고 생각했다.

개인적으로 좀 더 해보고 싶었던 건 전화번호를 쓰는 란에 :invalid로 형식에 맞지 않는 것을 넣으면 스타일링이 바뀌는데 여기서 더 발전시켜서 아예 입력이 되지 않게 해보고 싶었다. 좀 더 공부해 봐야겠다.

이번에 프로젝트를 하면서는 달력 부분의 스타일링에서 크나큰 고난을 겪었는데(다른 팀원분께서) 이걸 처음부터 만드는 것은 시간 상 무리여서 있는 라이브러리를 활용하여 기업과제에 있는 것과 최대한 스타일링을 비슷하게 하려고 노력하는 과정에서 고생을 많이 하셨다. 평소에 서비스를 이용하면서는 아무렇지도 않게 사용했던 기본적인 것들이 사실 직접 만들려고 하다보면 말도 안 되게 복잡하다는 것을 코드 공부하면서 계속 깨닫고 있는 나날인 것 같다.


import css from "styled-jsx/css";
import { IoIosArrowBack } from "react-icons/io";
import { useRouter } from "next/router";
import { useAppDispatch } from "redux/store";
import { reset } from "redux/slice";

const style = css`
  div {
    color: #5b5555;
    font-size: 16px;
    font-weight: 600;
  }
  .wrapper {
    height: 56px;
    padding: 0 10px;
    display: grid;
    * {
      grid-row: 1;
      grid-column: 1;
    }
  }
  .header {
    margin-bottom: 6px;
    place-self: center;
  }
  .goBack {
    place-self: center start;
    &:hover {
      cursor: pointer;
    }
  }
`;

const HeaderTop = () => {
  const router = useRouter();
  const dispatch = useAppDispatch();

  const resetStore = () => {
    if (confirm("현재까지의 데이터가 사라집니다. 계속 하시겠습니까?")) {
      dispatch(reset());
      router.push("/");
    }
  };

  return (
    <div className="wrapper">
      <div className="goBack" onClick={resetStore}>
        <IoIosArrowBack size={"2em"} color={"#5B5555"} />
      </div>
      <div className="header">돌보미 신청하기</div>
      <style jsx>{style}</style>
    </div>
  );
};

export default HeaderTop;
import { ReactElement } from "react";
import css from "styled-jsx/css";

const style = css`
  span:nth-child(1) {
    color: #5b5555;
    font-weight: 500;
    margin-right: 7px;
  }
  span:nth-child(2) {
    color: #ff8450;
  }
  span:nth-child(3) {
    color: #d3d2d2;
    margin-left: 5px;
  }
  .message-wrapper {
    margin: 26px 16px 26px 16px;
  }
  .care-message {
    color: #5b5555;
    font-size: 24px;
    font-weight: 500;
    margin-top: 16px;
  }
`;

interface IInfo {
  careTitle: string;
  pageNumber: string;
  pageFullNumber: string;
  careText: ReactElement | string;
}

const HeaderBottom = ({
  careTitle,
  pageNumber,
  pageFullNumber,
  careText,
}: IInfo) => {
  return (
    <div className="wrapper">
      <div className="message-wrapper">
        <span>{careTitle}</span>
        <span>{pageNumber}</span>
        <span>{pageFullNumber}</span>
        <div className="care-message">{careText}</div>
      </div>
      <style jsx>{style}</style>
    </div>
  );
};

export default HeaderBottom;
import { useRouter } from "next/router";
import { reset } from "redux/slice";
import { useAppDispatch } from "redux/store";
import css from "styled-jsx/css";

const style = css`
  .footer-wrapper {
    text-align: center;
    display: grid;
    grid-template-columns: 60px 300px;
    margin-left: 10px;
  }
  button:nth-child(1) {
    font-weight: 700;
    font-size: 14px;
    text-align: center;
    width: 50px;
    height: 48px;
    color: #7d7878;
    background-color: #ffffff;
  }
  button:nth-child(2) {
    background: #e2e2e2;
    color: #b6b3b3;
    width: 278px;
    height: 48px;
    border-radius: 4px;
  }
  .active:nth-child(2) {
    background: #ff8450;
    color: #ffffff;
  }
`;

interface IFooter {
  onNext: () => void;
  active: boolean;
}

const Footer = ({ onNext, active }: IFooter) => {
  const router = useRouter();
  const dispatch = useAppDispatch();

  const resetStore = () => {
    if (confirm("현재까지의 데이터가 사라집니다. 계속 하시겠습니까?")) {
      dispatch(reset());
      router.push("/");
    }
  };

  return (
    <div className="footer-wrapper">
      <button onClick={resetStore}>이전</button>
      <button
        onClick={onNext}
        className={active ? "active" : ""}
        disabled={active ? false : true}
      >
        다음
      </button>
      <style jsx>{style}</style>
    </div>
  );
};

export default Footer;

import css from "styled-jsx/css";
import HeaderBottom from "../components/HeaderBottom";
import HeaderTop from "../components/HeaderTop";
import Footer from "../components/Footer";
import { useState } from "react";
import { workType as workTypeAction } from "redux/slice";
import { useRouter } from "next/router";
import { useAppDispatch } from "redux/store";

const style = css`
  .apply-wrapper {
    display: grid;
    grid-template-rows: 50px 150px;
    grid-auto-rows: min-content;
    height: 100%;
  }
  .care-type {
    width: 160px;
    height: 144px;
    border: 1px solid lightgrey;
    border-radius: 3px;
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    text-align: center;
    margin: 10px 8px 10px 10px;
    cursor: pointer;
    span:nth-child(1) {
      font-size: 55px;
    }
    span:nth-child(2) {
      font-size: 15px;
      color: #5b5555;
      font-weight: 500;
    }
  }
  .active {
    background-color: #ff8450;
    span:nth-child(2) {
      color: #ffffff;
    }
  }
  footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    margin-bottom: 10px;
  }
`;

const apply = () => {
  const [workType, setWorkType] = useState("");
  const dispatch = useAppDispatch();
  const router = useRouter();

  const handleNextPage = () => {
    if (workType) {
      dispatch(workTypeAction(workType));
      router.push("/schedule");
    }
  };

  return (
    <div className="apply-wrapper">
      <HeaderTop />
      <HeaderBottom
        careTitle={"돌봄 유형"}
        pageNumber={"1"}
        pageFullNumber={"/ 4"}
        careText={"돌봄 유형을 설정해주세요"}
      />
      <div>
        <div
          className={workType === "DAY" ? "care-type active" : "care-type"}
          onClick={() => setWorkType("DAY")}
        >
          <span>🌞</span>
          <span>24시간 상주</span>
        </div>
        <div
          className={workType === "TIME" ? "care-type active" : "care-type"}
          onClick={() => setWorkType("TIME")}
        >
          <span></span>
          <span>시간제 돌봄</span>
        </div>
      </div>
      <footer>
        <Footer onNext={handleNextPage} active={!!workType} />
      </footer>

      <style jsx>{style}</style>
    </div>
  );
};

export default apply;

import css from "styled-jsx/css";
import HeaderBottom from "../components/HeaderBottom";
import HeaderTop from "../components/HeaderTop";
import Footer from "../components/Footer";
import { useAppDispatch, useAppSelector } from "redux/store";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { phoneNumber } from "redux/slice";
import useSWR from "swr";
import { post } from "./api/addr";

const style = css`
  .apply-wrapper {
    display: grid;
    grid-template-rows: 50px;
    grid-auto-rows: min-content;
    height: 100%;
  }
  .applyfin-info-box {
    box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.05),
      0px 0px 5px rgba(0, 0, 0, 0.05);
    border-radius: 8px;
    width: 320px;
    justify-content: center;
    align-items: center;
    align-content: center;
    margin: 0 auto;
    padding: 30px 16px 20px 16px;
  }
  .input-wrapper {
    margin-top: 16px;
  }
  input {
    box-shadow: rgba(0, 0, 0, 0.02) 0px 1px 3px 0px,
      rgba(27, 31, 35, 0.15) 0px 0px 0px 1px;
    border-radius: 4px;
    border: gray;
    width: 328px;
    height: 48px;
    display: block;
    margin: 0 auto;
  }
  input:invalid {
    border: 1px solid red;
  }
  div {
    color: #5b5555;
  }
  hr {
    border-top: 1px solid #f6f6f6;
    margin: 10px 0 10px 0;
  }
  .apply-content {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 30px;
  }
  .care-top {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 15px;
  }
  .care-bottom {
    font-size: 14px;
    font-weight: 300;
    margin: 10px 0px 10px 0px;
  }
  footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    margin-bottom: 10px;
  }
`;

const applyfin = () => {
  const router = useRouter();
  const state = useAppSelector((state) => state);
  const dispatch = useAppDispatch();
  const visitTime = Number(state.schedule.visitTime.split(":")[0]);
  const [phoneNum, setPhoneNum] = useState<string>("");
  const [isActive, setIsActive] = useState(false);
  const { startDate, endDate } = state.schedule;

  const formatTime = () => {
    if (visitTime === 12) {
      return `오후 ${visitTime}시부터`;
    } else {
      if (visitTime > 12) {
        return `오후 ${visitTime - 12}시부터`;
      } else {
        return `오전 ${visitTime}시부터`;
      }
    }
  };

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const {
      target: { value },
    } = e;
    const regExp = new RegExp(/^01([0|1|6|7|8|9])([0-9]{3,4})([0-9]{4})$/);

    if (regExp.test(value)) {
      setIsActive(true);
    } else {
      setIsActive(false);
    }

    setPhoneNum(value);
  };

  const handleNextPage = () => {
    dispatch(phoneNumber(phoneNum));
    router.push("/finish");
  };

  const { data, mutate } = useSWR("submit", () => post(state));

  useEffect(() => {
    (async () => {
      const res = await mutate(state);

      // 받은 res의 데이터 중 _id로 POST 조회 가능
    })();
  }, [state.phoneNumber]);

  return (
    <div>
      <div className="apply-wrapper">
        <HeaderTop />
        <HeaderBottom
          careTitle={"신청완료"}
          pageNumber={"4"}
          pageFullNumber={"/ 4"}
          careText={
            <>
              <p>인증하신 휴대폰 번호로</p>
              <p>케어코디 프로필을</p>
              <p>받아보실 수 있어요☺️</p>
            </>
          }
        />
        <div className="applyfin-info-box">
          <div className="apply-content">신청 내역</div>
          <div className="care-top">돌봄 유형</div>
          <div className="care-bottom">
            {state.workType === "TIME" ? "⏰ 시간제 돌봄" : "🌞 24시간 상주"}
          </div>
          <hr></hr>
          <div className="care-top">돌봄 일정</div>
          <div className="care-bottom">
            {startDate.split("-")[0]}{startDate.split("-")[1]}{" "}
            {startDate.split("-")[2]}~ {endDate.split("-")[0]}{" "}
            {endDate.split("-")[1]}{endDate.split("-")[2]}</div>
          <div className="care-bottom">{formatTime()}</div>
          <div className="care-bottom">{`${state.schedule.hour}시간`}</div>
          <hr></hr>
          <div className="care-top">돌봄 주소</div>
          <div className="care-bottom">{state.address.roadAddress}</div>
          <div className="care-bottom">{state.address.addressDetail}</div>
        </div>
        <div className="input-wrapper">
          <input
            placeholder="전화번호를 입력해주세요(숫자만 입력해주세요)"
            type="text"
            pattern="[0-9]+"
            value={phoneNum}
            onChange={onChange}
          />
        </div>
        <footer>
          <Footer onNext={handleNextPage} active={isActive} />
        </footer>
        <style jsx>{style}</style>
      </div>
    </div>
  );
};

export default applyfin;

0개의 댓글