2차 프로젝트 기술 회고 - CalPick

mia·2023년 2월 11일
2

예약 페이지

1) Selected.js

  • 하나의 컨테이너에 title의 형태와 배경색이 같은 component인 SelecteForm을 각각 width값과 title에 맞게 props로 정보를 넘겨주어 페이지 구현.
<S.SelectedContainer className="inner">
      <SelectForm w="45%" title="예약하기">
        <UserInfo seat={seat} seatId={seatId} params={params} />
      </SelectForm>
      <SelectForm w="45%" title="좌석 선택">
        <SelectSeat onButtonClick={selectSeat} params={params} />
      </SelectForm>
    </S.SelectedContainer>

2) UserInfo.jsx

  • INPUT_LIST라는 상수 데이터에 id와 title, type, 함수를 넣어주어 map함수와 함께 input에 넣어줌.
    test가 이름에 포함되어있다면 fetch로 받을 데이터이기 때문에 input창을 disabled 시켜줌.
    UserInfoContainer 상위에 div대신 key값을 받을 수 있는 태그가 필요하여 React.Fragment를 사용했다. (React.Fragment..? => <></>의 풀 버전!)
const INPUT_LIST = [
  { id: 'testName', title: '시험' },
  { id: 'testDate', title: '시험 일정' },
  {
    id: 'testsiteName',
    title: '고사장',
  },
  { id: 'testSeats', title: '좌석' },
  { id: 'name', title: '이름' },
  {
    id: 'phone',
    title: '휴대폰',
    type: 'number',
    handleKeyDown: e => {
      if (e.keyCode === 69) e.preventDefault();
    },
  },
  {
    id: 'phoneCheck',
    title: '인증번호',
    placeholder: '숫자(6자리)',
    type: 'number',
    handleKeyDown: e => {
      if (e.keyCode === 69) e.preventDefault();
    },
  },
  { id: 'email', title: '이메일' },
];

<React.Fragment key={id}>
	<S.Inputs
              id={id}
              disabled={id.includes('test')}
              placeholder={placeholder}
              name={id}
              value={userInfo.info[id]}
              onChange={userInfo.handleInput}
              type={type}
              onKeyDown={handleKeyDown}
            />
</React.Fragment>
  • 상위에 userInfo라는 이름으로 useInput 훅을 불러오며 initialState 값은 INPUT_LIST에 map함수를 사용해 id의 값만 가져온 배열을 만든 후 reduce함수를 통해 각각의 id값을 key값으로 가지는 objectd의 value 값을 빈 스트링으로 줌.
const userInfo = useInput(initialState);

const KEYS = INPUT_LIST.map(({ id }) => id);
const initialState = KEYS.reduce((result, id) => ({ ...result, [id]: '' }), {});
  • input의 값이 바뀌는 경우 onChange함수를 사용해 해당 name 값(id)에 맞는 value를 변경해줌.
  • number type인 phone과 phone check(핸드폰, 번호 확인)은 알파벳 e를 숫자로 인식(지수)하기 때문에 핸드폰 번호를 체크하는 과정에는 e가 필요하지 않아 onKeyDown 이벤트에 keyCode가 69라면 e.preventDefault함수를 통해 눌리는 동작이 실행되지 않도록 설정.
  • warning msg : inputMessageCondition라는 object에 각 아이디 값을 key값으로 메세지를 value값으로 설정하여 {inputMessageCondition[id]}로 일치하는 id값이 있다면 value를 보여줌.
const isPhoneVaild = phone.length >= 9;
  const codeUnderSix = phoneCheck.length < 6;
  const codeOverTen = phoneCheck.length > 10;
  const isEmailVaild = email.includes('@');
  const isAllChecked =
    testSeats !== '' &&
    name !== '' &&
    isPhoneVaild &&
    phoneCheck !== '' &&
    isEmailVaild;

  const inputMessageCondition = {
    phone: isPhoneVaild ? '가능합니다!' : '9자 이상 가능합니다.(숫자만)',
    phoneCheck:
      (codeUnderSix && '6자리를 입력해주세요.') ||
      (codeOverTen && '9자 이상 가능합니다.(숫자만)'),
    email: !isEmailVaild && '이메일 주소를 다시 확인해주세요.',
  };
  • Timer : 핸드폰 인증 버튼을 눌렀을 때 시간을 보여줄 수 있는 타이머 기능 구현. 초를 나타내는 s와 분을 나타내는 m으로 설정 후 setInterval로 s가 0보다 크다면 1초씩 줄임. s가 0이고 m도 0이라면 clearInterval로 timer를 끔. s가 0, m은 0이 아니라면 1분 줄이고 s를 59로 변경. 초를 두자리 수로 출력하기 위해 0을 먼저 붙인 후 뒤에서 두번째까지 slice한 값을 보여줌.
const handlePhoneCheckBtn = e => {
    e.preventDefault();
    let s = parseInt(seconds);
    let m = parseInt(minutes);
    timer.current = setInterval(() => {
      if (s > 0) {
        setSeconds(--s);
      }
      if (s === 0) {
        if (m === 0) {
          clearInterval(timer.current);
        } else {
          setMinutes(--m);
          setSeconds(59);
          s = 59;
        }
      }
    }, 1000);
  • 토스페이 결제 : 토스 간편결제를 이용했으며 홈페이지에 자세한 설명이 나와있다.
    토스 간편결제
    다만 버튼을 눌렀을 때 간편결제로 바로 가야하는 플로우가 아니었기 때문에 조금의 가공이 필요했다.
    결제 플로우 : 버튼클릭(백엔드로 상품 이름, 가격, 좌석 정보 넘겨줌) -> 백엔드에서 결제 관련 정보(토스에서 필요로 하는 정보)를 넘겨줌 -> 백엔드에서 받은 정보를 토스결제로 넘겨줌.
const handleSubmitInfo = e => {
    e.preventDefault();
    fetch(`${API.order}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
        Authorization: process.env.REACT_APP_AUTHORIZATION,
      },
      body: JSON.stringify({
        orderName: userInfo.info.testName,
        totalAmount: 3000,
        testId: seatId,
        seatInfo: testSeats,
      }),
    })
      .then(res => res.json())
      .then(data => {
        loadTossPayments(clientKey).then(tossPayments => {
          tossPayments.requestPayment('카드', data).catch(error => {
            if (error.code === 'USER_CANCEL') {
              navigate('/order?payment=user_cancel');
            } else if (error.code === 'INVAILD_CARD_COMPANY') {
              navigate('/order?payment=invaild_card');
            }
          });
        });
      });
  };

3) SelectSeat.jsx

  • 교실 레이아웃 : grid를 활용하여 칠판, 창문, 문, 좌석의 위치를 잡아줌
export const SelectSeatContainer = styled.div`
  display: grid;
  grid-template-columns: 10% 82% 8%;
  grid-template-rows: 10% 90%;
  grid-template-areas:
    'a board b'
    'windows seats door';
  ${props => props.theme.variables.wh('100%', '55vh')}
  background-color: white;
  border-radius: 5px;
`;
  • 좌석 레이아웃 : grid를 활용 + 들어오는 정보가 배열안의 배열안의 객체로 들어오기 때문에 두번의 map함수를 사용하여 구현
export const Seats = styled.div`
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-template-rows: repeat(5, 1fr);
  grid-gap: 15px;
  grid-area: seats;
  margin: 30px;
`;

 <S.Seats>
        {seats.map(seat =>
          seat.map(({ id, seatInfo, isBooked }) => (
            <S.Seat key={id} isbooked={isBooked}>
              <MdOutlineChairAlt
                size="32px"
                onClick={() => clickSeat(seatInfo, isBooked, id)}
              />
            </S.Seat>
          ))
        )}
      </S.Seats>
  • 좌석 선택 기능 구현 : 좌석 클릭시 isBooked가 0이라면(isBooked는 예약된 상태를 나타내며 빈좌석은 0, 예약된 좌석은 1로 들어옴) state끌어올리기를 통해 seatInfo와 id값을 보내줌.
    changed라는 변수에 좌석 정보를 복사한 값을 넣은 후 targetRow(가로줄)안에 선택한 id값이 있다면 그 줄을 넣어주고 targetCol(세로줄)에서 해당 줄에 id 값이 있는 객체를 찾아 넣어줌.
    changed[targetRow][targetCol].isBooked = 1;로 변경.
    마지막으로 setSeats(changed)로 좌석 정보를 업데이트 해줌.
    isBooked가 1이라면 좌석을 선택할 수 없다는 alert를 띄워줌.
const clickSeat = (seatInfo, isBooked, id) => {
    if (!isBooked) {
      onButtonClick(seatInfo, id);
      const changed = [...seats];
      const targetRow = changed.findIndex(seats =>
        seats.some(seat => seat.id === id)
      );
      const targetCol = changed[targetRow].findIndex(seat => seat.id === id);
      changed[targetRow][targetCol].isBooked = 1;
      setSeats(changed);
    } else {
      alert('해당 좌석을 선택할 수 없습니다');
    }
  };

결제 완료 페이지

Order.js

searchParams를 사용하여 payment상태를 가져옴.
결제가 정상적으로 이루어졌을 때는 url에 payment가 뜨지 않으며 결제창이 닫혔을 때와 유효하지 않은 카드를 사용했을 때는 각각의 값에 따른 payment의 값이 쿼리스트링으로 가도록 설정해두었다.
받아온 값에 따라 조건부 렌더링으로 말풍선과 icon을 다르게 설정했다.

export default function Order() {
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  const goToMain = () => navigate('/');
  const payment = searchParams.get('payment');

  return (
    <S.OrderContainer className="inner">
      <S.BubbleContainer>
        <S.OrderBubble>
          {(!payment && '접수가 완료되었습니다.') ||
            (payment === 'user_cancel' && '결제창이 닫혔습니다.') ||
            (payment === 'invaild_card' && '유효하지 않은 카드입니다.')}
        </S.OrderBubble>
      </S.BubbleContainer>
      <S.OrderIcons>
        {payment ? <FcCancel size="48px" /> : <FcCheckmark size="48px" />}
      </S.OrderIcons>
      <S.OrderMessage>
        캘픽 시험문제는 CalPick 고유 창작물로서 저작권 법에 의해 보호받는
        저작물이며
        <br /> 해당 저작물과 관련된 모든 권리는 저작권자인 CalPick에 귀속됩니다.
      </S.OrderMessage>
      <Button size="medium" color="primary" onClick={goToMain}>
        메인 페이지
      </Button>
    </S.OrderContainer>
  );
}

자격증 출력 페이지

Certification.js

react-to-pdf 라이브러리 사용했으며 codepen의 도움을 많이 받았다.
react-to-pdf
pdf로 전환을 원하는 제일 상위 태그에 ref를 주고 파일변환해주는 버튼의 targetRef로 해당 ref를 준다. toPdf 함수를 click event로 넘겨줌.

export default function Certification() {
  const [certifi, setCertifi] = useState([]);
  const ref = useRef();

  useEffect(() => {
    fetch('./data/certification.json')
      .then(res => res.json())
      .then(data => setCertifi(data));
  }, []);

  return (
    <S.PageContainer>
      <S.CertificationContainer ref={ref}>
        <S.CertificationHeader>
          <S.CalpickLogo src="./images/logo.png" alt="캘픽로고" />
          <S.CertificationTitle>
            OFFICIAL SCORE CERTIFICATION
          </S.CertificationTitle>
        </S.CertificationHeader>
        <S.CertificationBody>
          <S.CertificationInfo>
            {certifi.map(({ id, title, data }) => (
              <S.InfoCard key={id}>
                <S.InfoTitle>{title}</S.InfoTitle>
                <S.InfoContents>{data}</S.InfoContents>
              </S.InfoCard>
            ))}
          </S.CertificationInfo>
          <S.CertificationScore>
            <S.ScoreTilte>SCORE</S.ScoreTilte>
            <S.Score>900</S.Score>
          </S.CertificationScore>
          <S.ScoreVerify src="./images/verify.png" />
        </S.CertificationBody>
      </S.CertificationContainer>
      <Pdf targetRef={ref} filename="하평안_캘픽자격증.pdf">
        {({ toPdf }) => (
          <Button size="medium" color="primary" clickHandler={toPdf}>
            pdf파일저장 & 출력하기
          </Button>
        )}
      </Pdf>
    </S.PageContainer>
  );
}
profile
노 포기 킾고잉

0개의 댓글