<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>
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>
const userInfo = useInput(initialState);
const KEYS = INPUT_LIST.map(({ id }) => id);
const initialState = KEYS.reduce((result, id) => ({ ...result, [id]: '' }), {});
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 && '이메일 주소를 다시 확인해주세요.',
};
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');
}
});
});
});
};
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;
`;
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>
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('해당 좌석을 선택할 수 없습니다');
}
};
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>
);
}
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>
);
}