원티드 X 코드스테이츠 프리온보딩 프론트엔드 과정 기업과제 6번
담당했던 부분 : Header, Footer 컴포넌트 작성 / 시작, 돌봄 유형 선택, 정보 확인, 마지막 페이지 작성
✨ 주요 기능
이번 프로젝트에서는 재사용되는 컴포넌트들과 페이지들을 주로 작업하였다. 그 전의 프로젝트들에서는 재사용되는 컴포넌트들을 만들게 되어도 자꾸 재사용성을 생각하지 않고 그냥 만드는 무지성적인 일을 해서 팀원분들이 뚝딱뚝딱 도와주셨는데 이번에는 처음에 만들 때부터 "재사용한다...! 재사용한다...!" 이러면서 작업해서 재사용성에 맞게 작업할 수 있었다!
그리고 타입스크립트와 스타일드 컴포넌트를 같이 쓰면 타입스크립트가 너무 느려지는 사태가 자꾸 발생했어서 이번에는 좀 더 가볍다는 스타일드-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;