diff --git a/FE/airbnb/public/index.html b/FE/airbnb/public/index.html index d16d1a6f2..0f2f85fde 100644 --- a/FE/airbnb/public/index.html +++ b/FE/airbnb/public/index.html @@ -13,5 +13,6 @@
+ diff --git a/FE/airbnb/src/App.tsx b/FE/airbnb/src/App.tsx index 048beee13..8c153c63b 100644 --- a/FE/airbnb/src/App.tsx +++ b/FE/airbnb/src/App.tsx @@ -21,9 +21,7 @@ function App() { - - - + diff --git a/FE/airbnb/src/components/header/form/FormGuest.tsx b/FE/airbnb/src/components/header/form/FormGuest.tsx index 9383b3307..5100c60d4 100644 --- a/FE/airbnb/src/components/header/form/FormGuest.tsx +++ b/FE/airbnb/src/components/header/form/FormGuest.tsx @@ -8,7 +8,6 @@ import FormGuestToggle from './guestToggle/FormGuestToggle'; import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; import { guestState, isFormOpenedState, reserveInfoSelector } from '../../../recoil/headerAtom'; import { ReactComponent as DeleteBtn } from '../../../assets/svg/Property 1=x-circle.svg'; -import { Link } from 'react-router-dom'; import ConditionalLink from '../../util/ConditionalLink'; import { reserveInfoType, clientReserveAPI } from '../../../util/api'; @@ -45,7 +44,6 @@ const FormGuest = () => { }; const handleSubmitClick = (e: MouseEvent): void => { - console.log(reserveInfo); e.stopPropagation(); }; diff --git a/FE/airbnb/src/components/header/form/FormPrice.tsx b/FE/airbnb/src/components/header/form/FormPrice.tsx index ebfa21998..8e340d357 100644 --- a/FE/airbnb/src/components/header/form/FormPrice.tsx +++ b/FE/airbnb/src/components/header/form/FormPrice.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useEffect, useRef } from 'react'; +import { MouseEvent, Suspense, useEffect, useRef } from 'react'; import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'; import styled from 'styled-components'; import useToggle from '../../../hooks/useToggle'; @@ -54,7 +54,11 @@ const FormPrice = () => { {isShowDeleteBtn && open && } - {open && } + {open && ( + + + + )} ); }; diff --git a/FE/airbnb/src/components/header/form/priceBar/PriceBar.tsx b/FE/airbnb/src/components/header/form/priceBar/PriceBar.tsx index 2b25326b7..ab1b307a0 100644 --- a/FE/airbnb/src/components/header/form/priceBar/PriceBar.tsx +++ b/FE/airbnb/src/components/header/form/priceBar/PriceBar.tsx @@ -5,8 +5,9 @@ import PriceChart from './PriceChart'; import { btnPositionType, priceSectionType } from './priceType'; import { ReactComponent as PauseBtn } from '../../../../assets/svg/Property 1=pause-circle.svg'; import { priceData as sampleData } from './sampleData'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { + fetchPrice, pauseBtnLastPositionState, pauseBtnPositionState, priceState, @@ -32,7 +33,9 @@ const PriceBar = ({ toggleRef }: Props) => { const [btnLastPosition, setBtnLastPosition] = useRecoilState(pauseBtnLastPositionState); const [priceRange, setPriceRange] = useRecoilState(priceState); const [priceData, setPriceData] = useState(sampleData); + // const priceData = useRecoilValue(fetchPrice); + console.log(priceData); const minPrice = getNumberWithComma(priceRange.min); const maxPrice = getNumberWithComma(priceRange.max); const priceAverage = getNumberWithComma(getPriceAverage(priceData)); diff --git a/FE/airbnb/src/components/header/form/priceBar/PriceChart.tsx b/FE/airbnb/src/components/header/form/priceBar/PriceChart.tsx index d4f5d8dda..3004d4359 100644 --- a/FE/airbnb/src/components/header/form/priceBar/PriceChart.tsx +++ b/FE/airbnb/src/components/header/form/priceBar/PriceChart.tsx @@ -34,8 +34,6 @@ const PriceChart = ({ priceSection }: Props) => { const drawDotsLine = (dots: dotType[]): void => { if (!canvas || !ctx) return; - const canvasWidth = canvas.width; - const canvasHeight = canvas.height; ctx.beginPath(); ctx.fillStyle = 'rgba(0,0,0,0.7)'; @@ -53,9 +51,9 @@ const PriceChart = ({ priceSection }: Props) => { } ctx.lineTo(prevX, prevY); - ctx.lineTo(canvasWidth, canvasHeight); - ctx.lineTo(0, canvasHeight); ctx.fill(); + ctx.strokeStyle = 'rgba(0,0,0,0.3)'; + ctx.stroke(); ctx.closePath(); }; diff --git a/FE/airbnb/src/components/reservePageSkeleton/MapSkeleton.tsx b/FE/airbnb/src/components/reservePageSkeleton/MapSkeleton.tsx new file mode 100644 index 000000000..36eac57b7 --- /dev/null +++ b/FE/airbnb/src/components/reservePageSkeleton/MapSkeleton.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; + +interface Props {} + +const MapSkeleton = (props: Props) => { + return loading; +}; + +export default MapSkeleton; + +const StyledMapSkeleton = styled.div` + background-color: #f2f2f2; + position: absolute; + right: 0; + width: 50%; + height: 100%; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to right, #f2f2f2, #ddd, #f2f2f2); + animation: loading 1s infinite linear; + } + + @keyframes loading { + 0% { + transform: translateX(0); + } + 50%, + 100% { + transform: translateX(460px); + } + } +`; diff --git a/FE/airbnb/src/components/reservePageSkeleton/ReserveSkeleton.tsx b/FE/airbnb/src/components/reservePageSkeleton/ReserveSkeleton.tsx new file mode 100644 index 000000000..33e1ed333 --- /dev/null +++ b/FE/airbnb/src/components/reservePageSkeleton/ReserveSkeleton.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +interface Props {} + +const ReserveSkeleton = (props: Props) => { + return ( + +
+
+
+
+
+
+ ); +}; + +export default ReserveSkeleton; + +const StyledReserveSkeleton = styled.div` + .skeleton__img, + .skeleton__info, + .skeleton__price { + background-color: f2f2f2; + } +`; diff --git a/FE/airbnb/src/components/reserveRoomList/ReserveRoom.tsx b/FE/airbnb/src/components/reserveRoomList/ReserveRoom.tsx index 6481f2db4..cd47f08bc 100644 --- a/FE/airbnb/src/components/reserveRoomList/ReserveRoom.tsx +++ b/FE/airbnb/src/components/reserveRoomList/ReserveRoom.tsx @@ -5,22 +5,21 @@ import { roomType } from './roomType'; import ReserveRoomGrade from './ReserveRoomGrade'; import ReserveRoomPrice from './ReserveRoomPrice'; import ReserveForm from './reserveForm/ReserveForm'; -import { useState } from 'react'; +import { useRef } from 'react'; +import useToggle from '../../hooks/useToggle'; interface Props { roomData: roomType; } const ReserveRoom = ({ roomData }: Props) => { - const [open, setOpen] = useState(false); const { photo } = roomData; - - const handleClick = () => { - setOpen(true); - }; + const clickRef = useRef(null); + const toggleRef = useRef(null); + const { open } = useToggle({ clickRef, toggleRef }); return ( - +
@@ -34,7 +33,7 @@ const ReserveRoom = ({ roomData }: Props) => { - {open && } + {open && }
); }; diff --git a/FE/airbnb/src/components/reserveRoomList/ReserveRoomList.tsx b/FE/airbnb/src/components/reserveRoomList/ReserveRoomList.tsx index bcef97be3..9c66cb14b 100644 --- a/FE/airbnb/src/components/reserveRoomList/ReserveRoomList.tsx +++ b/FE/airbnb/src/components/reserveRoomList/ReserveRoomList.tsx @@ -14,6 +14,7 @@ const ReserveRoomList = ({ className }: Props) => { const reserveRoomList = roomsData && roomsData.map((roomData, idx) => ); + // const reserveRoomList = roomsData && roomsData.map((roomData, idx) => ); return (
diff --git a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveBtn.tsx b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveBtn.tsx new file mode 100644 index 000000000..a0e8d4481 --- /dev/null +++ b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveBtn.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +interface Props { + className: string; +} + +const ReserveBtn = ({ className }: Props) => { + return 예약하기; +}; + +export default ReserveBtn; + +const StyledReserveBtn = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 55px; + border: none; + border-radius: 10px; + background-color: ${({ theme }) => theme.colors.black}; + color: ${({ theme }) => theme.colors.white}; + font-weight: 700; +`; diff --git a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveForm.tsx b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveForm.tsx index 96804639b..612227160 100644 --- a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveForm.tsx +++ b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveForm.tsx @@ -1,14 +1,39 @@ +import { RefObject } from 'react'; import styled from 'styled-components'; import { roomType } from '../roomType'; +import ReserveBtn from './ReserveBtn'; +import ReserveFormHeader from './ReserveFormHeader'; +import ReserveFormInfo from './ReserveFormInfo'; +import ReserveFromPrice from './ReserveFromPrice'; +import { createPortal } from 'react-dom'; +import usePortal from '../../../hooks/usePortal'; + interface Props { + toggleRef: RefObject; roomData: roomType; } -const ReserveForm = ({ roomData }: Props) => { +const ReserveForm = ({ toggleRef, roomData }: Props) => { + const { chargePerNight } = roomData; + const portalElement: HTMLDivElement | null = document.querySelector('#modal'); return ( - - 123; - + portalElement && + createPortal( + + + + + +
예약 확정 전에는 요금이 청구되지 않습니다.
+ +
+
, + portalElement + ) ); }; @@ -16,14 +41,34 @@ export default ReserveForm; const StyledReserveFormWrapper = styled.div` z-index: 10; - position: absolute; + position: fixed; + top: 0; width: 100%; height: 100%; + display: flex; + align-items: center; + justify-content: center; background-color: rgba(0, 0, 0, 0.3); `; const StyledReserveForm = styled.div` + padding: 36px 24px; width: 400px; - height: 500px; - background-color: green; + height: 525px; + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 10px; + box-shadow: 0px 4px 10px rgba(51, 51, 51, 0.1), 0px 0px 4px rgba(51, 51, 51, 0.05); + .reserve__header { + margin-bottom: 1.5rem; + } + .reserve__info, + .reserve__btn, + .reserve__warn { + margin-bottom: 1rem; + } + .reserve__warn { + text-align: center; + font-size: ${({ theme }) => theme.fontSize.small}; + color: ${({ theme }) => theme.colors.gray3}; + } `; diff --git a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormHeader.tsx b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormHeader.tsx new file mode 100644 index 000000000..b029f5aff --- /dev/null +++ b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormHeader.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +interface Props { + className?: string; + chargePerNight: number; + review: number; +} + +const ReserveFormHeader = ({ className, chargePerNight, review }: Props) => { + return ( + +
+ ₩{chargePerNight.toLocaleString()} + / 박 +
+
+ 후기 {review}개 +
+
+ ); +}; + +export default ReserveFormHeader; + +const StyledReserveFormHeader = styled.div` + display: flex; + align-items: flex-end; + justify-content: space-between; + + .price { + font-size: ${({ theme }) => theme.fontSize.large}; + font-weight: bold; + } + .review { + font-size: ${({ theme }) => theme.fontSize.small}; + color: ${({ theme }) => theme.colors.gray3}; + text-decoration: underline; + } +`; diff --git a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormInfo.tsx b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormInfo.tsx new file mode 100644 index 000000000..168865e6e --- /dev/null +++ b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFormInfo.tsx @@ -0,0 +1,77 @@ +import { useRecoilValue } from 'recoil'; +import styled from 'styled-components'; +import { selectDateState } from '../../../recoil/calendarAtom'; +import { guestState } from '../../../recoil/headerAtom'; +import { getDateByTime } from '../../header/form/calendar/calendarDateFn'; + +interface Props { + className?: string; +} + +const ReserveFormInfo = ({ className }: Props) => { + const selectDate = useRecoilValue(selectDateState); + const guestDate = useRecoilValue(guestState); + + const timeToReserveFormdate = (time: number | null): string => { + const date = getDateByTime(time); + if (!date) return ''; + return `${date.year}. ${date.month}. ${date.day}.`; + }; + + const checkInDate = timeToReserveFormdate(selectDate.checkIn); + const checkOutDate = timeToReserveFormdate(selectDate.checkOut); + const totalGuest = Object.values(guestDate).reduce((acc, cur) => acc + cur); + + return ( + +
+
+
체크인
+
{checkInDate}
+
+
+
체크아웃
+
{checkOutDate}
+
+
+
+
+
인원
+
게스트 {totalGuest}명
+
+
+
+ ); +}; + +export default ReserveFormInfo; + +const StyledReserveFormInfo = styled.div` + border-radius: 10px; + .reserve-form__column { + display: flex; + align-items: center; + } + .reserve-form__item { + padding: 8px; + .reserve-form__title { + font-size: ${({ theme }) => theme.fontSize.small}; + font-weight: 700; + } + .reserve-form__info { + color: ${({ theme }) => theme.colors.gray2}; + } + } + border: ${({ theme }) => `1px solid ${theme.colors.gray4}`}; + .reserve-form__column:first-child { + } + .reserve-form__date { + flex: 1; + } + .reserve-form__date:first-child { + border-right: ${({ theme }) => `1px solid ${theme.colors.gray4}`}; + } + .reserve-form__column:last-child { + border-top: ${({ theme }) => `1px solid ${theme.colors.gray4}`}; + } +`; diff --git a/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFromPrice.tsx b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFromPrice.tsx new file mode 100644 index 000000000..1b354f7e5 --- /dev/null +++ b/FE/airbnb/src/components/reserveRoomList/reserveForm/ReserveFromPrice.tsx @@ -0,0 +1,87 @@ +import { useRecoilValue } from 'recoil'; +import styled from 'styled-components'; +import { selectDateState } from '../../../recoil/calendarAtom'; +import { getBetweenDays } from '../../header/form/calendar/calendarDateFn'; + +interface Props { + chargePerNight: number; +} + +const ReserveFromPrice = ({ chargePerNight }: Props) => { + const selectDate = useRecoilValue(selectDateState); + + const getCleanCharge = (totalPrice: number): number => Math.floor(totalPrice * 0.03); + const getServiceCharge = (totalPrice: number): number => Math.floor(totalPrice * 0.15); + const getTaxCharge = (totalPrice: number): number => Math.floor(totalPrice * 0.01); + + const betweenDays = getBetweenDays(selectDate.checkIn, selectDate.checkOut); + const totalPrice = chargePerNight * betweenDays; + + const cleanCharge = getCleanCharge(totalPrice); + const serviceCharge = getServiceCharge(totalPrice); + const taxCharge = getTaxCharge(totalPrice); + + const totalCharge = totalPrice + cleanCharge + serviceCharge + taxCharge; + + return ( + + +
+
+ ₩{chargePerNight.toLocaleString()} x {betweenDays}박 +
+
₩{totalPrice.toLocaleString()}
+
+
+
청소비
+
₩{cleanCharge.toLocaleString()}
+
+
+
서비스 수수료
+
₩{serviceCharge.toLocaleString()}
+
+
+
숙박세와 수수료
+
₩{taxCharge.toLocaleString()}
+
+
+ +
+
총 합계
+
₩{totalCharge.toLocaleString()}
+
+
+
+ ); +}; + +export default ReserveFromPrice; + +const StyledReservePriceWrapper = styled.div` + .reserve-price__column { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + } + .reserve-price__title { + text-decoration: underline; + } +`; + +const StyledReserveFormPrice = styled.div` + padding-bottom: 1rem; + border-bottom: ${({ theme }) => `2px solid ${theme.colors.gray5}`}; + margin-bottom: 1rem; +`; + +const StyledReserveCharge = styled.div` + font-weight: 700; + .reserve-price__column { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + } + .reserve-price__title { + text-decoration: underline; + } +`; diff --git a/FE/airbnb/src/components/util/ConditionalLink.tsx b/FE/airbnb/src/components/util/ConditionalLink.tsx index 080b1cd3e..70cf17561 100644 --- a/FE/airbnb/src/components/util/ConditionalLink.tsx +++ b/FE/airbnb/src/components/util/ConditionalLink.tsx @@ -7,6 +7,6 @@ interface Props { } const ConditionalLink = ({ children, to, condition }: Props) => - !!condition && to ? {children} : <>{children}; + condition && to ? {children} : <>{children}; export default ConditionalLink; diff --git a/FE/airbnb/src/pages/ReservePage.tsx b/FE/airbnb/src/pages/ReservePage.tsx index 3cf4593f7..178ea7f0a 100644 --- a/FE/airbnb/src/pages/ReservePage.tsx +++ b/FE/airbnb/src/pages/ReservePage.tsx @@ -1,8 +1,9 @@ -import { useEffect } from 'react'; +import { Suspense, useEffect } from 'react'; import { useRecoilValue, useRecoilState } from 'recoil'; import styled from 'styled-components'; import Map from '../components/map/Map'; import ReserveHeader from '../components/reserveHeader/ReserveHeader'; +import MapSkeleton from '../components/reservePageSkeleton/MapSkeleton'; import ReserveRoomList from '../components/reserveRoomList/ReserveRoomList'; import { reserveInfoSelector } from '../recoil/headerAtom'; import { getRoomsSelector } from '../recoil/reserveRoomAtom'; @@ -11,8 +12,6 @@ interface Props {} const ReservePage = ({}: Props) => { const [reserveInfo, setReserveInfo] = useRecoilState(reserveInfoSelector); - const roomsData = useRecoilValue(getRoomsSelector); - console.log(roomsData); useEffect(() => { const [encodedAddress, checkIn, checkOut, minCharge, maxCharge, adult, child, infants] = getQueryValue(window.location.search); @@ -35,8 +34,12 @@ const ReservePage = ({}: Props) => { return ( - - + + + + }> + + ); }; diff --git a/FE/airbnb/src/recoil/headerAtom.ts b/FE/airbnb/src/recoil/headerAtom.ts index a5c11d261..b5021cda0 100644 --- a/FE/airbnb/src/recoil/headerAtom.ts +++ b/FE/airbnb/src/recoil/headerAtom.ts @@ -1,5 +1,7 @@ import { atom, selector } from 'recoil'; +import { timeToDate } from '../components/header/form/calendar/calendarDateFn'; import { guestStateType } from '../components/header/form/guestToggle/guestType'; +import { serverAPI } from '../util/api'; import { selectDateState } from './calendarAtom'; export const tabSelectedState = atom({ @@ -61,6 +63,21 @@ export interface reserveQueryType { guests: guestStateType; } +export const fetchPrice = selector({ + key: 'get/price', + get: async ({ get }) => { + const city = get(locationState); + const date = get(selectDateState); + if (!city || !date.checkIn || !date.checkOut) + return '도시, 체크인, 체크아웃 날짜를 입력해주세요'; + const checkIn = date.checkIn; + const checkOut = date.checkOut; + const response = await fetch(serverAPI.getPrice({ city, checkIn, checkOut })); + const data = await response.json(); + return data.data.charges; + }, +}); + export const reserveInfoSelector = selector({ key: 'reserveInformation', get: ({ get }): reserveQueryType => { diff --git a/FE/airbnb/src/recoil/reserveRoomAtom.ts b/FE/airbnb/src/recoil/reserveRoomAtom.ts index 3e47249d5..4a4c144e6 100644 --- a/FE/airbnb/src/recoil/reserveRoomAtom.ts +++ b/FE/airbnb/src/recoil/reserveRoomAtom.ts @@ -1,6 +1,7 @@ import { atom, selector } from 'recoil'; import { roomType } from '../components/reserveRoomList/roomType'; import { serverAPI } from '../util/api'; +import { delay } from '../util/util'; import { reserveInfoSelector, reserveQueryType } from './headerAtom'; import { roomData } from './roomSampleData'; @@ -20,7 +21,8 @@ export const getRoomsSelector = selector({ if (isUndefined(reserveInfo)) return null; const response = await fetch(serverAPI.getRooms(reserveInfo)); const data = await response.json(); - return data; + + return delay(1000, data); }, }); diff --git a/FE/airbnb/src/util/api.ts b/FE/airbnb/src/util/api.ts index bb00ced6b..f875b91e9 100644 --- a/FE/airbnb/src/util/api.ts +++ b/FE/airbnb/src/util/api.ts @@ -9,6 +9,11 @@ export interface reserveInfoType { maxCharge: number; guests: guestStateType; } +interface priceArgType { + city: string; + checkIn: number; + checkOut: number; +} interface apiType { url: string; @@ -20,7 +25,9 @@ interface apiType { maxCharge, guests, }: reserveInfoType) => string; + getPrice: ({ city, checkIn, checkOut }: priceArgType) => string; } + export const serverAPI: apiType = { url: 'http://13.125.35.62', getRooms: ({ address, checkIn, checkOut, minCharge, maxCharge, guests }) => { @@ -30,6 +37,12 @@ export const serverAPI: apiType = { const query = `city=${address}&check_in=${checkInDate}&check_out=${checkOutDate}&min_charge=${minCharge}&max_charge=${maxCharge}&guests=${guestNumber}`; return serverAPI.url + '/accommodations?' + query; }, + getPrice: ({ city, checkIn, checkOut }) => { + const checkInDate = timeToDate(checkIn); + const checkOutDate = timeToDate(checkOut); + const query = `city=${city}&check_in=${checkInDate}&check_out=${checkOutDate}`; + return serverAPI.url + '/accommodations/charges?' + query; + }, }; export const clientReserveAPI = ({ diff --git a/FE/airbnb/src/util/util.js b/FE/airbnb/src/util/util.js index d1c77545e..58cbf9575 100644 --- a/FE/airbnb/src/util/util.js +++ b/FE/airbnb/src/util/util.js @@ -14,3 +14,6 @@ export const getUrlParams = () => { return params; }; + +export const delay = (time, value = '') => + new Promise((resolve) => setTimeout(() => resolve(value), time));