- 프로젝트 주제 : 숙박 예약 웹서비스 구현 프로젝트
- 프로젝트 기간 : 11월 20일(월) ~ 12월 01일(목)
- 주요 목표 : UI/UX 작업, React.js 기반 컴포넌트 단위 구조 설계, API 명세서에 따른 JSON 데이터 화면 출력, 무한 스크롤
const instance = axios.create({
baseURL: SERVER_URL,
headers: {
Accept: 'application/json',
},
});
instance.interceptors.request.use(
(config) => {
const accessToken = getCookie('accessToken');
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
import instance from './axios';
export const postJoin = async (
email: string,
password: string,
name: string,
) => {
const res = await instance.post('members/signup', {
email: email,
password: password,
name: name,
});
return res.data;
};
export const validationSignUpSchema = yup.object({
mail: yup
.string()
.matches(/^[^@\s]+@[^\s]+\.[^@\s]+$/, '잘못된 이메일 형식입니다.')
.required('이메일은 필수 입니다.'),
name: yup
.string()
.matches(
/^[a-zA-Z가-힣]{2,16}$/,
'영문자 및 한글로 2자 이상 16자 이하로 입력해주세요.',
)
.required('이름은 필수 입니다.'),
pw: yup
.string()
.matches(
/^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}$/,
'최소 한 개이상의 숫자, 특수문자, 영문자 및 8자 이상 20자 이하로 입력해주세요.',
)
.required('비밀번호는 필수 입니다.'),
checkPw: yup
.string()
.oneOf([yup.ref('pw')], '비밀번호가 일치하지 않습니다.')
.required('비밀번호는 필수 입니다.'),
});
import supertest from 'supertest';
const request = supertest('https://api.hitit7mini.store');
test('POST /api/members/signup', async () => {
const userData = {
email: 'emailaaaaa@hanmail.net',
password: 'password1232!',
name: '마이콜',
};
const signUp = await request // 회원가입
.post('/api/members/signup')
.set('Content-Type', 'application/json')
.send(userData);
expect(signUp.status).toEqual(201); // success
// expect(signUp.status).toEqual(400); // duplicated or error
});
import React from 'react';
// import Header from 'components/signUp/Header';
import Inner from 'components/signUp/Inner';
import CommonHeader from 'components/common/CommonHeader';
const SignUp = () => {
return (
<div className="min-h-screen bg-bgGray">
<CommonHeader name="회원가입" />
{/* <Header /> */}
<Inner />
</div>
);
};
export default SignUp;
import React, { useState, useRef, useEffect } from 'react';
import { Cancel, Visibility, VisibilityOff, Block } from '@mui/icons-material';
import { Input } from '@material-tailwind/react';
import { useNavigate } from 'react-router-dom';
import { useFormik } from 'formik';
import { validationSignUpSchema } from 'utils/validateSchema';
import styles from '../../components/login/Login.module.css';
import { postJoin } from '../../apis/signUpAPI';
import { useRecoilValue } from 'recoil';
import { signUpModalState } from 'recoil/atoms/signUpModalAtom';
import SignUpModal from 'components/login/SignUpModal';
import swal from 'sweetalert';
const Inner = () => {
const navigate = useNavigate();
const mailRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const pwRef = useRef<HTMLInputElement>(null);
const checkPwRef = useRef<HTMLInputElement>(null);
const [showPw, setShowPw] = useState(false);
const [showCheckPw, setShowCheckPw] = useState(false);
const [showName, setShowName] = useState(false);
const [showPwVis, setShowPwVis] = useState(false);
const [showCheckPwVis, setShowCheckPwVis] = useState(false);
const [showMail, setShowMail] = useState(false);
const [showModal, setShowModal] = useState(true);
const modalCheck = useRecoilValue(signUpModalState);
const cancelCloseMailHandler = (event: MouseEvent) => {
if (mailRef.current && !mailRef.current.contains(event.target as Node)) {
setShowMail(false);
}
};
const cancelOpenMailHandler = (event: MouseEvent) => {
if (mailRef.current && mailRef.current.contains(event.target as Node)) {
setShowMail(true);
}
};
const cancelCloseNameHandler = (event: MouseEvent) => {
if (nameRef.current && !nameRef.current.contains(event.target as Node)) {
setShowName(false);
}
};
const cancelOpenNameHandler = (event: MouseEvent) => {
if (nameRef.current && nameRef.current.contains(event.target as Node)) {
setShowName(true);
}
};
const visClosePwHandler = (event: MouseEvent) => {
if (pwRef.current && !pwRef.current.contains(event.target as Node)) {
setShowPwVis(false);
}
};
const visOpenPwHandler = (event: MouseEvent) => {
if (pwRef.current && pwRef.current.contains(event.target as Node)) {
setShowPwVis(true);
}
};
const visCloseCheckPwHandler = (event: MouseEvent) => {
if (
checkPwRef.current &&
!checkPwRef.current.contains(event.target as Node)
) {
setShowCheckPwVis(false);
}
};
const visOpenCheckPwHandler = (event: MouseEvent) => {
if (
checkPwRef.current &&
checkPwRef.current.contains(event.target as Node)
) {
setShowCheckPwVis(true);
}
};
useEffect(() => {
window.addEventListener('click', cancelCloseMailHandler);
window.addEventListener('click', cancelOpenMailHandler);
return () => {
window.removeEventListener('click', cancelCloseMailHandler);
window.removeEventListener('click', cancelOpenMailHandler);
};
}, [showMail]);
useEffect(() => {
window.addEventListener('click', cancelCloseNameHandler);
window.addEventListener('click', cancelOpenNameHandler);
return () => {
window.removeEventListener('click', cancelCloseNameHandler);
window.removeEventListener('click', cancelOpenNameHandler);
};
}, [showName]);
useEffect(() => {
window.addEventListener('click', visClosePwHandler);
window.addEventListener('click', visOpenPwHandler);
return () => {
window.removeEventListener('click', visClosePwHandler);
window.removeEventListener('click', visOpenPwHandler);
};
}, [showPwVis]);
useEffect(() => {
window.addEventListener('click', visCloseCheckPwHandler);
window.addEventListener('click', visOpenCheckPwHandler);
return () => {
window.removeEventListener('click', visCloseCheckPwHandler);
window.removeEventListener('click', visOpenCheckPwHandler);
};
}, [showCheckPwVis]);
const formik = useFormik({
initialValues: {
mail: '',
name: '',
pw: '',
checkPw: '',
},
validationSchema: validationSignUpSchema,
onSubmit: async (values) => {
try {
swal({ title: '회원가입에 성공했습니다.', icon: 'success' });
await postJoin(values.mail, values.pw, values.name);
navigate('/login');
} catch (e: any) {
let errorMessage = '';
if (e.response.data.error.message === '이미 존재하는 이메일') {
errorMessage = '이미 가입된 이메일입니다.';
} else {
for (let i = 0; i < e.response.data.error.data.length; i++) {
if (e.response.data.error.data[i].field === 'email') {
errorMessage = '잘못된 이메일 형식입니다.';
} else if (e.response.data.error.data[i].field === 'name') {
errorMessage = '잘못된 이름 형식입니다.';
} else {
errorMessage = '잘못된 비밀번호 형식입니다.';
}
}
}
swal({ title: errorMessage, icon: 'warning' });
}
},
});
const handleOnClose = () => {
setShowModal(false);
};
const { values, touched, errors, handleChange, handleBlur, handleSubmit } =
formik;
return (
<>
{modalCheck === false && (
<SignUpModal open={showModal} onClose={handleOnClose}></SignUpModal>
)}
<div className="pt-20 min-h-screen m-auto bg-white max-w-[768px] mx-auto">
<div className="pt-4.5 pl-8 pr-8">
<form className="m-0 p-0" onSubmit={handleSubmit}>
<div className="relative">
<div className="mb-8">
<div className="pt-11 flex">
<Input
type="text"
variant="standard"
label="이메일"
name="mail"
value={values.mail}
onChange={handleChange}
onBlur={handleBlur}
error={touched.mail && Boolean(errors.mail)}
ref={mailRef}
crossOrigin={undefined}
/>
{values.mail && showMail && (
<Cancel
className="cursor-pointer right-0 absolute"
onClick={() => {
formik.setFieldValue('mail', '');
}}
></Cancel>
)}
</div>
{values.mail.length > 0 && errors.mail && (
<div className="text-sm flex text-red items-center">
<Block className="pr-1" />
{errors.mail}
</div>
)}
<div className="pt-11 flex">
<Input
type="text"
variant="standard"
label="이름"
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
error={touched.name && Boolean(errors.name)}
ref={nameRef}
crossOrigin={undefined}
/>
{values.name && showName && (
<Cancel
className="cursor-pointer right-0 absolute"
onClick={() => {
formik.setFieldValue('name', '');
}}
></Cancel>
)}
</div>
{values.name.length > 0 && errors.name && (
<div className="text-sm flex text-red items-center">
<Block className="pr-1" />
{errors.name}
</div>
)}
<div className="pt-11 flex" ref={pwRef}>
<Input
type={showPw ? 'text' : 'password'}
variant="standard"
label="비밀번호"
name="pw"
value={values.pw}
onChange={handleChange}
onBlur={handleBlur}
error={touched.pw && Boolean(errors.pw)}
crossOrigin={undefined}
/>
{values.pw && !showPw && showPwVis && (
<Visibility
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowPw(!showPw);
}}
></Visibility>
)}
{values.pw && showPw && showPwVis && (
<VisibilityOff
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowPw(!showPw);
}}
></VisibilityOff>
)}
</div>
{values.pw.length > 0 && errors.pw && (
<div className="text-sm text-red flex items-center">
<Block className="pr-1" />
{errors.pw}
</div>
)}
<div className="pt-11 flex" ref={checkPwRef}>
<Input
type={showCheckPw ? 'text' : 'password'}
variant="standard"
label="비밀번호 확인"
name="checkPw"
value={values.checkPw}
onChange={handleChange}
onBlur={handleBlur}
error={touched.checkPw && Boolean(errors.checkPw)}
crossOrigin={undefined}
/>
{values.checkPw && !showCheckPw && showCheckPwVis && (
<Visibility
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowCheckPw(!showCheckPw);
}}
></Visibility>
)}
{values.checkPw && showCheckPw && showCheckPwVis && (
<VisibilityOff
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowCheckPw(!showCheckPw);
}}
></VisibilityOff>
)}
</div>
{values.checkPw.length > 0 && errors.checkPw && (
<div className="text-sm text-red flex items-center">
<Block className="pr-1" />
{errors.checkPw}
</div>
)}
</div>
</div>
{!errors.mail &&
!errors.pw &&
!errors.name &&
!errors.checkPw &&
values.mail.length > 0 &&
values.pw.length > 0 &&
values.name.length > 0 &&
values.checkPw.length > 0 ? (
<button
type="submit"
style={{
width: '100%',
boxShadow: 'none',
fontFamily: 'AppleSDGothicNeoL',
color: 'white',
fontWeight: 700,
fontSize: '18px',
border: '1px solid #de2e5f',
borderRadius: '5px',
}}
className={styles.btnActive}
>
완료
</button>
) : (
<div
style={{
width: '100%',
boxShadow: 'none',
fontFamily: 'AppleSDGothicNeoL',
color: 'white',
fontWeight: 700,
fontSize: '18px',
border: '1px solid #ccc',
borderRadius: '5px',
}}
className="mt-4 pt-3 pb-3 flex items-center justify-center bg-gray cursor-default"
>
완료
</div>
)}
</form>
</div>
</div>
</>
);
};
export default Inner;
import instance from './axios';
export const postLogin = async (email: string, password: string) => {
const res = await instance.post('members/login', {
email: email,
password: password,
});
return res.data.data;
};
export const validationLoginSchema = yup.object({
mail: yup
.string()
.matches(/^[^@\s]+@[^\s]+\.[^@\s]+$/, '잘못된 이메일 형식입니다.')
.required('이메일은 필수 입니다.'),
pw: yup
.string()
.matches(
/^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}$/,
'최소 한 개이상의 숫자, 특수문자, 영문자 및 8자 이상 20자 이하로 입력해주세요.',
)
.required('비밀번호는 필수 입니다.'),
});
import supertest from 'supertest';
const request = supertest('https://api.hitit7mini.store');
test('POST /api/members/login', async () => {
const userData = {
email: 'emailaaaaa@hanmail.net',
password: 'password1232!',
name: '마이콜',
};
const login = await request // 로그인
.post('/api/members/login')
.set('Content-Type', 'application/json')
.send(userData);
expect(login.status).toEqual(200); // success
// expect(response.status).toEqual(400); // duplicated or error
// console.log(login.body);
const accessToken = login.body.data.accessToken; // 여기서 나온 토큰으로 밑에서 Authorization으로 토큰 값 넣는 요청 처리하시면 됩니다.
const getUserInfo = await request // 회원 정보 조회
.get('/api/members/mypage')
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${accessToken}`);
expect(getUserInfo.status).toEqual(200); // success
const getUserOrderInfo = await request // 회원 주문 내역 조회
.get('/api/members/orders')
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${accessToken}`);
expect(getUserOrderInfo.status).toEqual(200); // success
});
import React from 'react';
// import Header from 'components/login/Header';
import Inner from 'components/login/Inner';
import { alreadyLogin } from 'hooks/common/isAcessToken';
import CommonHeader from 'components/common/CommonHeader';
const Login = () => {
alreadyLogin();
return (
<div className="min-h-screen bg-bgGray">
<CommonHeader name="이메일로 로그인" />
{/* <Header /> */}
<Inner />
</div>
);
};
export default Login;
import React, { useState, useRef, useEffect } from 'react';
import {
Cancel,
Visibility,
VisibilityOff,
ChevronRight,
Block,
} from '@mui/icons-material';
import { Input } from '@material-tailwind/react';
import { useNavigate } from 'react-router-dom';
import SignUpModal from './SignUpModal';
import styles from '../../components/login/Login.module.css';
import { validationLoginSchema } from 'utils/validateSchema';
import { useFormik } from 'formik';
import { useRecoilState } from 'recoil';
import { signUpModalState } from 'recoil/atoms/signUpModalAtom';
import { postLogin } from '../../apis/loginAPI';
import swal from 'sweetalert';
import { removeCookie, setCookie } from 'utils';
import { dateState } from 'recoil/atoms/myPageAtom';
const Inner = () => {
const navigate = useNavigate();
const mailRef = useRef<HTMLInputElement>(null);
const pwRef = useRef<HTMLInputElement>(null);
const [showPwVis, setShowPwVis] = useState(false);
const [showPw, setShowPw] = useState(false);
const [showMail, setShowMail] = useState(false);
const [showModal, setShowModal] = useState(false);
const [modalCheck, setModalCheck] = useRecoilState(signUpModalState);
const [date, setDate] = useRecoilState(dateState);
useEffect(() => {
setModalCheck(false);
}, []);
const cancelCloseHandler = (event: MouseEvent) => {
if (mailRef.current && !mailRef.current.contains(event.target as Node)) {
setShowMail(false);
}
};
const cancelOpenHandler = (event: MouseEvent) => {
if (mailRef.current && mailRef.current.contains(event.target as Node)) {
setShowMail(true);
}
};
const visCloseHandler = (event: MouseEvent) => {
if (pwRef.current && !pwRef.current.contains(event.target as Node)) {
setShowPwVis(false);
}
};
const visOpenHandler = (event: MouseEvent) => {
if (pwRef.current && pwRef.current.contains(event.target as Node)) {
setShowPwVis(true);
}
};
const formik = useFormik({
initialValues: {
mail: '',
pw: '',
},
validationSchema: validationLoginSchema,
onSubmit: async (values) => {
removeCookie();
try {
swal({ title: '로그인에 성공했습니다.', icon: 'success' });
const res = await postLogin(values.mail, values.pw);
setDate('최근 3개월');
const { accessToken } = res;
setCookie(accessToken);
navigate('/');
} catch (e: any) {
let errorMessage = '';
if (e.message === 'Request failed with status code 401') {
errorMessage = '이메일과 비밀번호를 확인해주세요.';
} else {
for (let i = 0; i < e.response.data.error.data.length; i++) {
if (e.response.data.error.data[i].field === 'email') {
errorMessage = '잘못된 이메일 형식입니다.';
} else if (e.response.data.error.data[i].field === 'password') {
errorMessage = '잘못된 비밀번호 형식입니다.';
}
}
}
swal({ title: errorMessage, icon: 'warning' });
}
},
});
const { values, touched, errors, handleChange, handleBlur, handleSubmit } =
formik;
useEffect(() => {
window.addEventListener('click', cancelCloseHandler);
window.addEventListener('click', cancelOpenHandler);
return () => {
window.removeEventListener('click', cancelCloseHandler);
window.removeEventListener('click', cancelOpenHandler);
};
}, [showMail]);
useEffect(() => {
window.addEventListener('click', visCloseHandler);
window.addEventListener('click', visOpenHandler);
return () => {
window.removeEventListener('click', visCloseHandler);
window.removeEventListener('click', visOpenHandler);
};
}, [showPwVis]);
const handleOnClose = () => {
setShowModal(false);
};
return (
<div className="pt-20 min-h-screen m-auto bg-white max-w-[768px] mx-auto">
<div className="pt-4.5 pl-8 pr-8">
<form className="m-0 p-0" onSubmit={handleSubmit}>
<div className="relative">
<div className="mb-8">
<div className="pt-11 flex">
<Input
type="text"
variant="standard"
label="이메일"
name="mail"
value={values.mail}
onChange={handleChange}
onBlur={handleBlur}
error={touched.mail && Boolean(errors.mail)}
ref={mailRef}
crossOrigin={undefined}
/>
{values.mail && showMail && (
<Cancel
className="cursor-pointer right-0 absolute"
onClick={() => {
formik.setFieldValue('mail', '');
}}
></Cancel>
)}
</div>
<div className="pt-11 flex" ref={pwRef}>
<Input
type={showPw ? 'text' : 'password'}
variant="standard"
label="비밀번호"
name="pw"
value={values.pw}
onChange={handleChange}
onBlur={handleBlur}
error={touched.pw && Boolean(errors.pw)}
crossOrigin={undefined}
/>
{values.pw && !showPw && showPwVis && (
<Visibility
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowPw(!showPw);
}}
></Visibility>
)}
{values.pw && showPw && showPwVis && (
<VisibilityOff
className="cursor-pointer right-0 absolute"
onClick={(e) => {
e.stopPropagation();
setShowPw(!showPw);
}}
></VisibilityOff>
)}
</div>
</div>
</div>
{!errors.mail &&
!errors.pw &&
values.mail.length > 0 &&
values.pw.length > 0 ? (
<button
type="submit"
style={{
width: '100%',
boxShadow: 'none',
fontFamily: 'AppleSDGothicNeoL',
color: 'white',
fontWeight: 700,
fontSize: '18px',
border: '1px solid #de2e5f',
borderRadius: '5px',
}}
className={styles.btnActive}
>
로그인
</button>
) : (
<div
style={{
width: '100%',
boxShadow: 'none',
fontFamily: 'AppleSDGothicNeoL',
color: 'white',
fontWeight: 700,
fontSize: '18px',
border: '1px solid #ccc',
borderRadius: '5px',
}}
className="mt-4 pt-3 pb-3 flex items-center justify-center bg-gray cursor-default"
>
로그인
</div>
)}
</form>
<div
className="flex justify-center items-center "
onClick={() => {
setShowMail(!showModal);
}}
>
<span
className="text-content cursor-pointer flex justify-center items-center mt-6 w-auto hover:bg-bgGray"
onClick={() => setShowModal(!showModal)}
>
이메일로 회원가입
<ChevronRight className="flex cursor-pointer m-2 justify-center items-center ml-1"></ChevronRight>
</span>
</div>
{modalCheck === false && (
<SignUpModal open={showModal} onClose={handleOnClose}></SignUpModal>
)}
</div>
</div>
);
};
export default Inner;
import instance from './axios';
import { myOrder } from 'types/myPage.type';
export const getUserInfo = async () => {
const res = await instance.get('members/mypage');
return res.data.data;
};
export const getUserOrderInfo = async () => {
const res = await instance.get<myOrder>('members/orders');
return res.data;
};
import { getUserOrderInfo } from '../../apis/myPageAPI';
import { useQuery } from 'react-query';
const getMyPageData = async (i: number) => {
try {
if (i == 1) {
i = 12;
} else if (i == 2) {
i = 24;
}
const currentDate = new Date();
currentDate.setMonth(currentDate.getMonth() - i);
const { data } = await getUserOrderInfo();
const filteredData = data?.filter(
(order: { orderItems: any[]; orderDate: string | number | Date }) => {
return order.orderItems?.some(() => {
const criteria = new Date(order.orderDate);
return criteria >= currentDate;
});
},
);
return filteredData;
} catch (error) {
console.log(error);
}
};
const useQueryMyPage = (i: number) => {
const { data, isLoading } = useQuery(
[`myPage/${i}`],
() => getMyPageData(i),
{
staleTime: 60000,
},
);
return { data, isLoading };
};
export default useQueryMyPage;
import React from 'react';
// import Header from 'components/myPage/Header';
import Inner from 'components/myPage/Inner';
import { requireLogin } from 'hooks/common/isAcessToken';
import CommonHeader from 'components/common/CommonHeader';
const MyPage = () => {
requireLogin();
return (
<div className="min-h-screen bg-bgGray">
<CommonHeader name="My 야놀자" isCartIcon />
{/* <Header /> */}
<Inner />
</div>
);
};
export default MyPage;
import React, { useEffect, useState } from 'react';
import bannerRed from '../../assets/images/bannerRed.png';
import { ArrowDropDown } from '@mui/icons-material';
import DateModal from './DateModal';
import { useRecoilState, useRecoilValue } from 'recoil';
import { dateState } from 'recoil/atoms/myPageAtom';
import ReservationCard from './ReservationCard';
import ReservationCardHeader from './ReservationCardHeader';
import { logout } from 'utils';
import { useNavigate } from 'react-router-dom';
import { getUserInfo } from '../../apis/myPageAPI';
import TopBtn from 'components/common/TopBtn';
import useScrollToShow from 'hooks/common/handleScroll';
import useQueryMyPage from 'hooks/myPage/useQueryMyPage';
import MyPageSkeleton from './skeleton/MyPageSkeleton';
const Inner = () => {
const [now, setNow] = useState(3);
const navigate = useNavigate();
const [showDateModal, setShowDateModal] = useState(false);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const nowDate = useRecoilValue(dateState);
const [date, setDate] = useRecoilState(dateState);
const show = useScrollToShow(false, 200);
useEffect(() => {
setNow(date !== null ? parseInt(date.replace(/\D/g, ''), 10) : 0);
}, [date, setDate]);
const handleDateModalClose = () => {
setShowDateModal(false);
};
useEffect(() => {
const fetchData = async () => {
try {
const res = await getUserInfo();
const { email, name } = res;
setName(name);
setEmail(email);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
const { data, isLoading } = useQueryMyPage(now);
return (
<div className="pt-20 min-h-screen m-auto bg-white max-w-[768px] mx-auto">
<div className="lg:pt-4.5 lg:pl-6 lg:pr-6 pb-7">
<div className="flex justify-between items-center">
<div className="text-content font-bold text-black cursor-default">
{name}
</div>
<div
className="text-xxsm text-textGray cursor-pointer"
onClick={() => {
logout();
setDate('최근 3개월');
navigate('/');
}}
>
로그아웃
</div>
</div>
<div className="text-md text-textGray pt-3 cursor-default">{email}</div>
<div className="mt-[48px]">
<img
src={bannerRed}
alt="bannerRed"
className="w-full cursor-pointer"
style={{ borderRadius: 10 }}
onClick={() => {
window.location.href =
'https://www.yanolja.com/promotion/nol-promotion?eventcode=NOYLN';
}}
/>
<div className="flex items-center justify-between">
<div className="font-bold text-title pt-10 cursor-default">
예약 내역
</div>
<div className="pt-10 ">
<div
className="flex items-center w-full h-9 border border-gray rounded-md cursor-pointer justify-between hover:bg-lightGray pl-4"
onClick={() => setShowDateModal(true)}
>
{nowDate}
<ArrowDropDown />
</div>
</div>
</div>
</div>
</div>
<DateModal
open={showDateModal}
onClose={handleDateModalClose}
></DateModal>
{show && <TopBtn show={show} />}
{isLoading && <MyPageSkeleton />}
{data && data?.length > 0 && (
<div className="pr-6 pl-6">
{data?.map(
(
order: { orderDate: string; orderItems: any[] },
index: React.Key | null | undefined,
) => (
<div
key={index}
className="border border-gray rounded-lg mb-8 shadow-md overflow-hidden"
>
<ReservationCardHeader orderDate={order.orderDate} />
{order.orderItems?.map((item, itemIndex) => (
<React.Fragment key={itemIndex}>
<ReservationCard
code={item.code}
accomodationName={item.accommodation.name}
accomodationImage={item.accommodation.image}
checkinDate={item.checkinDate}
checkoutDate={item.checkoutDate}
isUsed={item.isUsed}
accomodationId={item.accommodation.id}
capacity={item.roomType.capacity}
roomName={item.roomType.name}
/>
</React.Fragment>
))}
</div>
),
)}
</div>
)}
{data?.length === 0 && (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<div className="m-0 auto text-title pb-1">
조회조건에 맞는 내역이 없습니다.
</div>
<div className="text-content text-textGray pb-2">
상품을 예약해보세요
</div>
<div
className="p-3 border border-blue rounded-md cursor-pointer text-blue text-content pl-10 pr-10"
onClick={() => {
navigate('/');
}}
>
홈으로 가기
</div>
</div>
</>
)}
</div>
);
};
export default Inner;
로그인 및 회원 가입 중복 요청 버그
로그인 및 회원 가입 버튼을 여러 번 누르면 클릭 수 만큼 요청이 되는 것을 방지하기 위해 클릭 후 swal창으로 막으려고 하였습니다.
하지만 swal이 팝업되기 전까지의 시간이 있어 여러 번 요청되는 것을 식별하였습니다.
그래서 요청을 하기 전에 swal창을 팝업하도록 수정한 결과 버튼을 누르자마자 swal창이 떠서 중복 요청을 방지할 수 있었습니다.
let errorMessage = '';
if (e.message === 'Request failed with status code 401') {
errorMessage = '이메일과 비밀번호를 확인해주세요.';
} else {
for (let i = 0; i < e.response.data.error.data.length; i++) {
if (e.response.data.error.data[i].field === 'email') {
errorMessage = '잘못된 이메일 형식입니다.';
} else if (e.response.data.error.data[i].field === 'password') {
errorMessage = '잘못된 비밀번호 형식입니다.';
}
}
}
let errorMessage = '';
if (e.response.data.error.message === '이미 존재하는 이메일') {
errorMessage = '이미 가입된 이메일입니다.';
} else {
for (let i = 0; i < e.response.data.error.data.length; i++) {
if (e.response.data.error.data[i].field === 'email') {
errorMessage = '잘못된 이메일 형식입니다.';
} else if (e.response.data.error.data[i].field === 'name') {
errorMessage = '잘못된 이름 형식입니다.';
} else {
errorMessage = '잘못된 비밀번호 형식입니다.';
}
}
}
import instance from './axios';
export const postLogin = async (email: string, password: string) => {
const res = await instance.post('members/login', {
email: email,
password: password,
});
return res.data.data;
};
import instance from './axios';
export const postJoin = async (
email: string,
password: string,
name: string,
) => {
const res = await instance.post('members/signup', {
email: email,
password: password,
name: name,
});
return res.data;
};
import instance from './axios';
import { myOrder } from 'types/myPage.type';
export const getUserInfo = async () => {
const res = await instance.get('members/mypage');
return res.data.data;
};
export const getUserOrderInfo = async () => {
const res = await instance.get<myOrder>('members/orders');
return res.data;
};
export interface reservationCardProps {
code: string;
accomodationName: string;
accomodationImage: string;
checkinDate: string;
checkoutDate: string;
isUsed: boolean;
accomodationId: number;
capacity: number;
roomName: string;
}
오랜만에 백엔드와 협업을 하게 되어 재밌고 유익하였습니다.
저희 팀 모두가 능력이 있으신 분들 같아 다양한 기능들을 기한보다 빠르게 마무리 할 수 있었던 것 같습니다.
프로젝트 매니지먼트부터 유닛 테스팅 까지 알차게 프로젝트를 진행한 것 같아 뿌듯했습니다.
오랜만에 Jest를 사용해서 버벅였는데 다음 프로젝트에서는 개발 중간 중간에 테스팅을 진행해야 겠다고 생각하였습니다.
모두들 고생 많으셨습니다 !!!