From b9cd2032899167c1cbdb20d508dffde28841d7e8 Mon Sep 17 00:00:00 2001 From: HyunJin <102955516+xxxjinn@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:50:13 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=93=B1=EB=A1=9D=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 89882 --- src/api/init/index.ts | 17 +- src/api/init/type.ts | 77 ++++---- src/api/sign-in/index.ts | 3 +- src/assets/data/postAccommodationData.json | 149 ++++++++-------- src/assets/data/postImageFileData.json | 31 ++-- src/components/init/ButtonContainer.tsx | 167 ++++++++++++------ .../init-accommodation-registration/type.ts | 1 + .../init/init-info-confirmation/RoomInfo.tsx | 31 +++- .../layout/init-layout/InitLayout.tsx | 23 +-- src/components/room/price-container/index.tsx | 5 +- src/constants/api/index.ts | 10 ++ src/constants/routes.ts | 6 +- src/mocks/init/index.ts | 2 +- .../init-accommodation-registration/index.tsx | 111 +++++++++--- src/pages/init-info-confirmation/index.tsx | 39 +--- src/pages/init-room-registration/index.tsx | 78 +++++--- src/pages/sign-in/index.tsx | 131 +++++++------- src/queries/init/index.ts | 36 +++- src/queries/sign-in/index.ts | 16 +- src/stores/init/atoms.ts | 1 + src/test/sign-in/index.tsx | 4 +- 21 files changed, 535 insertions(+), 403 deletions(-) diff --git a/src/api/init/index.ts b/src/api/init/index.ts index e62b2642..d5e0ab67 100644 --- a/src/api/init/index.ts +++ b/src/api/init/index.ts @@ -4,22 +4,15 @@ import { PostAccommodationParams, PostImageFile, } from './type'; -import { Response } from '@/types/api'; export const ACCOMMODATION_REGISTRATION_API = { postImageFile: (formData: FormData) => - instance.post>( - '/api/accommodations/images', - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, + instance.post('/api/accommodations/images', formData, { + headers: { + 'Content-Type': 'multipart/form-data', }, - ), + }), postAccommodationInfo: (params: PostAccommodationParams) => - instance.post>('/api/accommodations', { - params, - }), + instance.post('/api/accommodations', params), }; diff --git a/src/api/init/type.ts b/src/api/init/type.ts index a0600b73..b6b3c638 100644 --- a/src/api/init/type.ts +++ b/src/api/init/type.ts @@ -1,5 +1,5 @@ export type PostImageFile = { - urls: string[]; + urls: { url: string }[]; }; export type PostAccommodationParams = { @@ -8,9 +8,10 @@ export type PostAccommodationParams = { detailAddress: string; zipCode: string; description: string; - type: string; + category: string; images: { url: string }[]; - options: { + thumbnail: string; + option: { cooking: boolean; parking: boolean; pickup: boolean; @@ -28,9 +29,9 @@ export type PostAccommodationParams = { maxCapacity: number; checkInTime: string; checkOutTime: string; - count: number; + amount: number; images: { url: string }[]; - options: { + option: { airCondition: boolean; tv: boolean; internet: boolean; @@ -39,40 +40,38 @@ export type PostAccommodationParams = { }; export type PostAccommodation = { - message: string; - code?: number | undefined; - data: { - accommodationId: number; + accommodationId: number; + name: string; + address: string; + description: string; + category: string; + images: { id: number; url: string }[]; + option: { + cooking: boolean; + parking: boolean; + pickup: boolean; + barbecue: boolean; + fitness: boolean; + karaoke: boolean; + sauna: boolean; + sports: boolean; + seminar: boolean; + }; + rooms: { + id: number; + status: string; name: string; - address: string; - description: string; - type: string; - images: { url: string }[]; - options: { - cooking: boolean; - parking: boolean; - pickup: boolean; - barbecue: boolean; - fitness: boolean; - karaoke: boolean; - sauna: boolean; - sports: boolean; - seminar: boolean; + price: number; + defaultCapacity: number; + maxCapacity: number; + checkInTime: string; + checkOutTime: string; + amount: number; + images: { id: number; url: string }[]; + option: { + airCondition: boolean; + tv: boolean; + internet: boolean; }; - rooms: { - name: string; - price: number; - defaultCapacity: number; - maxCapacity: number; - checkInTime: string; - checkOutTime: string; - count: number; - images: { id: number; url: string }[]; - options: { - airCondition: boolean; - tv: boolean; - internet: boolean; - }; - }[]; - }; + }[]; }; diff --git a/src/api/sign-in/index.ts b/src/api/sign-in/index.ts index 97e90b3c..65e05346 100644 --- a/src/api/sign-in/index.ts +++ b/src/api/sign-in/index.ts @@ -1,9 +1,8 @@ import { MemberData } from '@api/sign-in/type'; import { instance } from '..'; import { SignInData } from './type'; -import { Response } from '@/types/api'; export const SIGN_IN_API = { postLogin: (data: SignInData) => - instance.post>('/api/auth/owners/signin', data), + instance.post('/api/auth/owners/signin', data), }; diff --git a/src/assets/data/postAccommodationData.json b/src/assets/data/postAccommodationData.json index 5b23385f..f162b37c 100644 --- a/src/assets/data/postAccommodationData.json +++ b/src/assets/data/postAccommodationData.json @@ -1,79 +1,82 @@ { - "message": "성공적으로 쿠폰이 발급되었습니다.", - "data": { - "accommodationId": 96, - "name": "해뜨는집 펜션", - "address": "제주특별자치도 서귀포시 성산읍 한도로 137", - "description": "모든 객실에서 제주 제 1경인 성산일출봉이 보이는 넓은 주차장을 겸비한 숙소이다. 근거리에 우도가 자리 잡고 있고 성산일출봉을 등에 지고 오조리 바닷가에서 조개 밭 체험도 가능하며 펜션에서 조개 잡는 도구도 빌릴 수 있다. 객실은 호텔형과 콘도형으로 이루어져 있으며 침대방과 온돌방 중 선택 가능하며 바다가 한눈에 들어오는 통유리창을 설치하여 객실에서 일출과 일몰을 모두 볼 수 있다.\n펜션에서는 낚시점, 배낚시 할인 혜택도 제공하고 있다. 펜션에서 출발해서 약 50~60분이면 한바퀴 돌아볼 수 있는 해뜨는집 올레길은 아침, 저녁 시간 산책코스로 제격이다. 주변 30분 이내 방문 가능한 관광지로 섭지코지, 성산일출봉, 석산봉, 신양해수욕장 등이 있다.", - "type": "PENSION_POOL_VILLA", - "images": [ - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - }, - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - } - ], - "options": { - "cooking": true, - "parking": true, - "pickup": true, - "barbecue": true, - "fitness": true, - "karaoke": true, - "sauna": true, - "sports": true, - "seminar": true + "accommodationId": 1, + "name": "해뜨는집 펜션", + "address": "제주특별자치도 서귀포시 성산읍 한도로 137", + "description": "모든 객실에서 제주 제 1경인 성산일출봉이 보이는 넓은 주차장을 겸비한 숙소이다. 근거리에 우도가 자리 잡고 있고 성산일출봉을 등에 지고 오조리 바닷가에서 조개 밭 체험도 가능하며 펜션에서 조개 잡는 도구도 빌릴 수 있다. 객실은 호텔형과 콘도형으로 이루어져 있으며 침대방과 온돌방 중 선택 가능하며 바다가 한눈에 들어오는 통유리창을 설치하여 객실에서 일출과 일몰을 모두 볼 수 있다.\n펜션에서는 낚시점, 배낚시 할인 혜택도 제공하고 있다. 펜션에서 출발해서 약 50~60분이면 한바퀴 돌아볼 수 있는 해뜨는집 올레길은 아침, 저녁 시간 산책코스로 제격이다. 주변 30분 이내 방문 가능한 관광지로 섭지코지, 성산일출봉, 석산봉, 신양해수욕장 등이 있다.", + "type": "PENSION_POOL_VILLA", + "images": [ + { + "id": 1, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" }, - "rooms": [ - { - "name": "봄", - "price": 100000, - "defaultCapacity": 2, - "maxCapacity": 6, - "checkInTime": "15:00", - "checkOutTime": "12:00", - "count": 5, - "images": [ - { - "id": 1, - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - }, - { - "id": 2, - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - } - ], - "options": { - "airCondition": true, - "tv": true, - "internet": true + { + "id": 2, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + } + ], + "options": { + "cooking": true, + "parking": true, + "pickup": true, + "barbecue": true, + "fitness": true, + "karaoke": true, + "sauna": true, + "sports": true, + "seminar": true + }, + "rooms": [ + { + "id": 1, + "name": "봄", + "price": 100000, + "defaultCapacity": 2, + "maxCapacity": 6, + "checkInTime": "15:00", + "checkOutTime": "12:00", + "amount": 5, + "status": "SELLING", + "images": [ + { + "id": 1, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + }, + { + "id": 2, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" } - }, - { - "name": "여름", - "price": 150000, - "defaultCapacity": 2, - "maxCapacity": 6, - "checkInTime": "15:00", - "checkOutTime": "12:00", - "count": 5, - "images": [ - { - "id": 3, - "url": "https://images/pension_room3_2.webp" - }, - { - "id": 4, - "url": "https://images/pension_room3_1.webp" - } - ], - "options": { - "airCondition": true, - "tv": true, - "internet": true + ], + "options": { + "airCondition": true, + "tv": true, + "internet": true + } + }, + { + "id": 2, + "name": "여름", + "price": 150000, + "defaultCapacity": 2, + "maxCapacity": 6, + "checkInTime": "15:00", + "checkOutTime": "12:00", + "status": "SELLING", + "amount": 5, + "images": [ + { + "id": 3, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + }, + { + "id": 4, + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" } + ], + "options": { + "airCondition": true, + "tv": true, + "internet": true } - ] - } + } + ] } diff --git a/src/assets/data/postImageFileData.json b/src/assets/data/postImageFileData.json index da2eb21f..3799ae77 100644 --- a/src/assets/data/postImageFileData.json +++ b/src/assets/data/postImageFileData.json @@ -1,19 +1,16 @@ { - "message": "성공적으로 이미지를 저장했습니다.", - "data": { - "urls": [ - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - }, - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - }, - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - }, - { - "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" - } - ] - } + "urls": [ + { + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + }, + { + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + }, + { + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + }, + { + "url": "http://tong.visitkorea.or.kr/cms/resource/77/2876777_image2_1.jpg" + } + ] } diff --git a/src/components/init/ButtonContainer.tsx b/src/components/init/ButtonContainer.tsx index 34bd13d7..dad1d380 100644 --- a/src/components/init/ButtonContainer.tsx +++ b/src/components/init/ButtonContainer.tsx @@ -1,4 +1,4 @@ -import { Button, Modal } from 'antd'; +import { Button, Modal, message } from 'antd'; import { styled } from 'styled-components'; import { useNavigate } from 'react-router-dom'; import { @@ -6,10 +6,18 @@ import { ButtonContainerStyledWrapperProps, } from './type'; import { TextBox } from '@components/text-box'; -import { useState } from 'react'; import { ROUTES } from '@/constants/routes'; -import { useSetRecoilState } from 'recoil'; -import { roomPrevButtonState } from '@stores/init/atoms'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { + isUpdatedAccommodationState, + roomPrevButtonState, + userInputValueState, +} from '@stores/init/atoms'; +import { useAccommodationInfo } from '@queries/init'; +import { AxiosError } from 'axios'; +import { PostAccommodationParams } from '@api/init/type'; +import { RESPONSE_CODE } from '@/constants/api'; +import { getCookie, setCookie } from '@hooks/sign-in/useSignIn'; export const ButtonContainer = ({ buttonStyle, @@ -27,10 +35,106 @@ export const ButtonContainer = ({ } }; - const [isModalOpen, setIsModalOpen] = useState(false); + const [userInputValue, setUserInputValue] = + useRecoilState(userInputValueState); + let accommodationId = -1; + const setIsUpdatedAccommodation = useSetRecoilState( + isUpdatedAccommodationState, + ); + + const imageUrls: { url: string }[] = userInputValue[0].images.map( + (image) => ({ url: image.url }), + ); - const handleModalOk = () => { - setIsModalOpen(false); + const postAccommodationParams: PostAccommodationParams = { + name: userInputValue[0].name, + address: userInputValue[0].address, + detailAddress: userInputValue[0].detailAddress, + zipCode: userInputValue[0].zipCode, + description: userInputValue[0].description, + category: userInputValue[0].type, + thumbnail: userInputValue[0].thumbnail, + images: imageUrls, + option: userInputValue[0].options, + rooms: userInputValue[0].rooms.map((room) => ({ + name: room.name, + price: room.price as number, + defaultCapacity: room.defaultCapacity as number, + maxCapacity: room.maxCapacity as number, + checkInTime: room.checkInTime, + checkOutTime: room.checkOutTime, + amount: room.count as number, + images: room.images.map((image) => ({ url: image.url })), + option: room.options, + })), + }; + + const { mutate: accommodationInfo } = useAccommodationInfo({ + onSuccess(data) { + accommodationId = data.data.accommodationId; + setIsUpdatedAccommodation(false); + + const cookieAccommodationId = getCookie('accommodationId'); + if (!cookieAccommodationId) setCookie('accommodationId', accommodationId); + + message.success(`${data.data.name} 숙소가 등록되었습니다.`); + navigate(`/${accommodationId}${ROUTES.MAIN}`); + + setUserInputValue([ + { + name: '', + address: '', + detailAddress: '', + zipCode: '', + description: '', + type: '', + images: [], + thumbnail: '', + options: { + cooking: false, + parking: false, + pickup: false, + barbecue: false, + fitness: false, + karaoke: false, + sauna: false, + sports: false, + seminar: false, + }, + rooms: [], + editRoomIndex: -1, + isAccommodationEdit: false, + }, + ]); + }, + onError(error) { + if (error instanceof AxiosError) { + message.error({ + content: '요청에 실패했습니다. 잠시 후 다시 시도해주세요', + style: { marginTop: '210px' }, + }); + } + if ( + error.response?.data.code === RESPONSE_CODE.INVALID_CATEGORY || + error.response?.data.code === + RESPONSE_CODE.EMPTY_ACCOMMODATION_IMAGES || + error.response?.data.code === RESPONSE_CODE.EMPTY_ROOM_INFO || + error.response?.data.code === RESPONSE_CODE.REQUEST_BODY_ERROR || + error.response?.data.code === RESPONSE_CODE.EMPTY_ROOM_IMAGES + ) { + message.error({ + content: '요청을 실패했습니다. 관리자에게 문의해주세요', + style: { marginTop: '210px' }, + }); + } + if (error.response?.data.code === RESPONSE_CODE.NOT_FOUND_MEMBER) { + navigate(ROUTES.SIGNIN); + } + }, + }); + + const handleConfirmModalOk = () => { + accommodationInfo(postAccommodationParams); }; const confirm = () => { @@ -68,7 +172,7 @@ export const ButtonContainer = ({ width: '576px', bodyStyle: { height: '621px' }, centered: true, - onOk: () => setIsModalOpen(true), + onOk: handleConfirmModalOk, }); }; @@ -120,35 +224,6 @@ export const ButtonContainer = ({ 추가하기 )} - - - - 환영합니다! - - - 레스케이프 호텔 숙소 -
- 등록이 완료되었습니다. -
-
- navigate(ROUTES.MAIN)} - > - 홈으로 이동 - -
); }; @@ -187,24 +262,6 @@ const StyledNextText = styled.div` margin: 130px 0 3px; `; -const StyledTextContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - - height: 507px; -`; - const StyledMiddleTextContainer = styled.div` text-align: center; `; - -const StyledToMainButton = styled(Button)` - width: 100%; - height: 46px; - - font-size: 20px; - font-weight: 700; -`; diff --git a/src/components/init/init-accommodation-registration/type.ts b/src/components/init/init-accommodation-registration/type.ts index 1c99be93..a8c61bc1 100644 --- a/src/components/init/init-accommodation-registration/type.ts +++ b/src/components/init/init-accommodation-registration/type.ts @@ -112,6 +112,7 @@ export type UserInputValue = { description: string; type: string; images: Image[]; + thumbnail: string; options: AccommodationOptions; rooms: Room[]; editRoomIndex?: number | undefined; diff --git a/src/components/init/init-info-confirmation/RoomInfo.tsx b/src/components/init/init-info-confirmation/RoomInfo.tsx index 66a91391..d2b39aee 100644 --- a/src/components/init/init-info-confirmation/RoomInfo.tsx +++ b/src/components/init/init-info-confirmation/RoomInfo.tsx @@ -4,8 +4,8 @@ import styled from 'styled-components'; import { RoomItem } from './RoomItem'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '@/constants/routes'; -import { useSetRecoilState } from 'recoil'; -import { addRoomState } from '@stores/init/atoms'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { addRoomState, userInputValueState } from '@stores/init/atoms'; export const RoomInfo = () => { const navigate = useNavigate(); @@ -16,15 +16,24 @@ export const RoomInfo = () => { navigate(ROUTES.INIT_ROOM_REGISTRATION); }; + const userInputValue = useRecoilValue(userInputValueState); + return ( - - 객실 정보 - - - + 객실추가 - + + + 객실 정보 + + + 최대 15개 까지 등록 가능합니다. + + + {userInputValue[0].rooms.length < 15 && ( + + + 객실추가 + + )} @@ -58,6 +67,12 @@ const StyledHeadContainer = styled.div` justify-content: space-between; `; +const StyledHeadTextContainer = styled.div` + display: flex; + gap: 8px; + align-items: center; +`; + const StyledButton = styled(Button)` font-size: 18px; font-weight: 700; diff --git a/src/components/layout/init-layout/InitLayout.tsx b/src/components/layout/init-layout/InitLayout.tsx index f97939a0..f31d11ab 100644 --- a/src/components/layout/init-layout/InitLayout.tsx +++ b/src/components/layout/init-layout/InitLayout.tsx @@ -6,7 +6,6 @@ import { Outlet, useLocation } from 'react-router-dom'; import { styled } from 'styled-components'; import { RouteConfigProps } from './type'; import couponLogo from '@assets/image/logo.png'; -import { LeftOutlined } from '@ant-design/icons'; export const InitLayout = () => { const location = useLocation(); @@ -33,24 +32,13 @@ export const InitLayout = () => { const { pageName = '숙소 등록하기', pageDesc = '숙소 정보를 알려주세요.' } = routeConfig[currentRoute as keyof typeof routeConfig] || {}; - const kindOfIcon = () => { - if ( - window.location.pathname === ROUTES.INIT || - window.location.pathname === ROUTES.INIT_INFO_CONFIRMATION - ) { - return ; - } else { - return ; - } - }; - return ( - {kindOfIcon()} - + + 빨리잡아! 쿠폰센터 @@ -110,8 +98,6 @@ const StyledHeaderTextWrapper = styled.div` display: flex; gap: 8px; align-items: center; - - cursor: pointer; `; const StyledHeadContentCotainer = styled.div` @@ -159,8 +145,3 @@ const StyledImage = styled.img` width: 26px; height: 15px; `; - -const StyledPrevButton = styled(LeftOutlined)` - color: ${colors.primary}; - font-size: '24px'; -`; diff --git a/src/components/room/price-container/index.tsx b/src/components/room/price-container/index.tsx index 77d2eabd..0dfbd4ed 100644 --- a/src/components/room/price-container/index.tsx +++ b/src/components/room/price-container/index.tsx @@ -15,7 +15,6 @@ export const PriceContainer = ({ header, form }: PriceContainerProps) => { useEffect(() => { setOutOfRangeError(null); form.setFieldValue('price', numericValue?.toLocaleString()); - if (!numericValue) return; if (numericValue < MIN_PRICE || numericValue > MAX_PRICE) { setOutOfRangeError('10,000~1,000,000까지만 입력 가능합니다.'); @@ -27,7 +26,9 @@ export const PriceContainer = ({ header, form }: PriceContainerProps) => { const cleanedStringValue = stringValue.replace(/[^0-9]/g, ''); if (cleanedStringValue.length !== 0) { - setNumericValue(Number(cleanedStringValue)); + const numericValue = Number(cleanedStringValue); + setNumericValue(numericValue); + form.setFieldValue('price', numericValue.toLocaleString()); } else { form.setFieldValue('price', ''); } diff --git a/src/constants/api/index.ts b/src/constants/api/index.ts index 0984c8b9..acbcb446 100644 --- a/src/constants/api/index.ts +++ b/src/constants/api/index.ts @@ -36,9 +36,19 @@ export const RESPONSE_CODE = { REFUND_FAIL: 6002, NOT_FOUND_POINT: 6003, INSUFFICIENT_POINT_BALANCE: 6004, + // COMMON DB_ERROR: 9000, REQUEST_BODY_ERROR: 9001, SERVER_ERROR: 9002, INVALID_DATE: 9003, + + // IMAGE SAVE + IMAGE_SAVE_FAIL: 2005, + + //ACCOMMODATION REGISTRATION + INVALID_CATEGORY: 2002, + EMPTY_ACCOMMODATION_IMAGES: 2003, + EMPTY_ROOM_IMAGES: 3007, + EMPTY_ROOM_INFO: 3006, } as const; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index c0297047..d862abde 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -5,9 +5,9 @@ export const ROUTES = { SIGNUP_SUCCESS: '/signup/success', POINT_DETAIL: '/point-detail', INIT: '/init', - INIT_ACCOMMODATION_REGISTRATION: '/init/accommodation-registration', - INIT_ROOM_REGISTRATION: '/init/room-registration', - INIT_INFO_CONFIRMATION: '/init/info-confirmation', + INIT_ACCOMMODATION_REGISTRATION: '/accommodation-registration', + INIT_ROOM_REGISTRATION: '/accommodation-registration/room-registration', + INIT_INFO_CONFIRMATION: '/accommodation-registration/info-confirmation', COUPON: '/coupon', COUPON_REGISTRATION: '/coupon-registration', MAIN: '/main', diff --git a/src/mocks/init/index.ts b/src/mocks/init/index.ts index dabe19eb..cd33d964 100644 --- a/src/mocks/init/index.ts +++ b/src/mocks/init/index.ts @@ -1,5 +1,5 @@ import { HttpResponse } from 'msw'; -import postAccommodationData from '@assets/data/accommodationsData.json'; +import postAccommodationData from '@assets/data/postAccommodationData.json'; import postImageFileData from '@assets/data/postImageFileData.json'; export const postAccommodationInfoResolver = () => { diff --git a/src/pages/init-accommodation-registration/index.tsx b/src/pages/init-accommodation-registration/index.tsx index fc110b88..b890bde1 100644 --- a/src/pages/init-accommodation-registration/index.tsx +++ b/src/pages/init-accommodation-registration/index.tsx @@ -21,12 +21,12 @@ import { ROUTES } from '@/constants/routes'; import { useNavigate } from 'react-router-dom'; import { useImageFile } from '@queries/init'; import { AxiosError } from 'axios'; -import { Image } from '@api/room/type'; import { UserInputValue, defaultAccommodation, } from '@components/init/init-accommodation-registration/type'; import { AccommodationCategoryProps } from '@components/init/type'; +import { RESPONSE_CODE } from '@/constants/api'; export const InitAccommodationRegistration = () => { const navigate = useNavigate(); @@ -84,6 +84,23 @@ export const InitAccommodationRegistration = () => { type = form.getFieldValue('accommodation-category'); } + const newImages = []; + + const urls = data.data.urls; + for (let i = 0; i < imageFiles.length; i++) { + const image = imageFiles[i]; + if (image.url !== '') { + newImages.push({ url: image.url }); + } // 이미 이미지 url이 있는 상태 + } + + for (let i = 0; i < urls.length; i++) { + const url = urls[i].url; + if (typeof url === 'string') { + newImages.push({ url }); + } + } + const updatedUserInputValue = { ...userInputValue, type, @@ -93,10 +110,32 @@ export const InitAccommodationRegistration = () => { zipCode: form.getFieldValue('accommodation-postCode'), description: form.getFieldValue('accommodation-desc'), options: selectedOptions, - images: data.data.data.urls as unknown as Image[], + images: newImages, + thumbnail: data.data.urls[0].url, }; return [updatedUserInputValue]; }); + + setUpdatedAccommodationInfo(true); + setSelectedOptions({ + cooking: false, + parking: false, + pickup: false, + barbecue: false, + fitness: false, + karaoke: false, + sauna: false, + sports: false, + seminar: false, + }); + setImageFiles([]); + setClickedPrevButton(false); + + if (userInputValue[0].isAccommodationEdit) { + navigate(ROUTES.INIT_INFO_CONFIRMATION); + } else { + navigate(ROUTES.INIT_ROOM_REGISTRATION); + } }, onError(error) { if (error instanceof AxiosError) { @@ -105,6 +144,12 @@ export const InitAccommodationRegistration = () => { style: { marginTop: '210px' }, }); } + if (error.response?.data.code === RESPONSE_CODE.IMAGE_SAVE_FAIL) { + message.error({ + content: '요청을 실패했습니다. 관리자에게 문의해주세요', + style: { marginTop: '210px' }, + }); + } }, }); @@ -113,11 +158,22 @@ export const InitAccommodationRegistration = () => { let shouldExecuteImageFile = false; - for (let index = 0; index < imageFiles.length; index++) { + for (let i = 0; i < imageFiles.length; i++) { + const image = imageFiles[i]; + if (image.file) shouldExecuteImageFile = true; + } + + for (let index = 0; index < 5; index++) { const image = imageFiles[index]; - if (image.file !== null) { - formData.append('image1', image.file); - shouldExecuteImageFile = true; + if (!image || image.file === null) { + // 등록한 적이 있거나 이미지 자체를 등록하지 않은 순서 + const emptyBlob = new Blob([], { type: 'application/octet-stream' }); + const nullFile = new File([emptyBlob], 'nullFile.txt', { + type: 'text/plain', + }); + formData.append(`image${index + 1}`, nullFile); + } else { + formData.append(`image${index + 1}`, image.file); } } @@ -145,32 +201,33 @@ export const InitAccommodationRegistration = () => { zipCode: form.getFieldValue('accommodation-postCode'), description: form.getFieldValue('accommodation-desc'), options: selectedOptions, - images: userInputValue[0].images, + images: imageFiles, + thumbnail: userInputValue[0].images[0].url, rooms: userInputValue[0].rooms, }; return [updatedUserInputValue]; }); - } - setUpdatedAccommodationInfo(true); - setSelectedOptions({ - cooking: false, - parking: false, - pickup: false, - barbecue: false, - fitness: false, - karaoke: false, - sauna: false, - sports: false, - seminar: false, - }); - setImageFiles([]); - setClickedPrevButton(false); - - if (userInputValue[0].isAccommodationEdit) { - navigate(ROUTES.INIT_INFO_CONFIRMATION); - } else { - navigate(ROUTES.INIT_ROOM_REGISTRATION); + setUpdatedAccommodationInfo(true); + setSelectedOptions({ + cooking: false, + parking: false, + pickup: false, + barbecue: false, + fitness: false, + karaoke: false, + sauna: false, + sports: false, + seminar: false, + }); + setImageFiles([]); + setClickedPrevButton(false); + + if (userInputValue[0].isAccommodationEdit) { + navigate(ROUTES.INIT_INFO_CONFIRMATION); + } else { + navigate(ROUTES.INIT_ROOM_REGISTRATION); + } } }; diff --git a/src/pages/init-info-confirmation/index.tsx b/src/pages/init-info-confirmation/index.tsx index 38af8255..2cdcefcc 100644 --- a/src/pages/init-info-confirmation/index.tsx +++ b/src/pages/init-info-confirmation/index.tsx @@ -1,14 +1,12 @@ import { ROUTES } from '@/constants/routes'; -import { PlusOutlined } from '@ant-design/icons'; import { ButtonContainer } from '@components/init/ButtonContainer'; import { AccommodationInfo } from '@components/init/init-info-confirmation/AccommodationInfo'; import { RoomInfo } from '@components/init/init-info-confirmation/RoomInfo'; -import { TextBox } from '@components/text-box'; +import { getCookie } from '@hooks/sign-in/useSignIn'; import { isUpdatedAccommodationState, userInputValueState, } from '@stores/init/atoms'; -import { Button } from 'antd'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -26,23 +24,11 @@ export const InitInfoConfirmation = () => { }, []); if (userInputValue[0].name === '') { - return ( - - - - 등록 정보가 없습니다. 숙소 등록부터 진행해주세요! - - } - type="primary" - onClick={() => navigate(ROUTES.INIT_ACCOMMODATION_REGISTRATION)} - > - 숙소 등록하러 가기 - - - - ); + const accommodationId = getCookie('accommodationId'); + if (accommodationId) navigate(`/${accommodationId}${ROUTES.MAIN}`); + else navigate(ROUTES.INIT); } + return ( @@ -64,18 +50,3 @@ const StyledWrapper = styled.div` margin-top: 204px; `; - -const StyledNoInputWrapper = styled.div` - display: flex; - flex-direction: column; - align-items: center; - margin: 100px 0; - gap: 32px; -`; - -const StyledButton = styled(Button)` - width: 300px; - height: 50px; - - font-size: 20px; -`; diff --git a/src/pages/init-room-registration/index.tsx b/src/pages/init-room-registration/index.tsx index 508a4c88..5fd746c3 100644 --- a/src/pages/init-room-registration/index.tsx +++ b/src/pages/init-room-registration/index.tsx @@ -27,9 +27,9 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; import styled from 'styled-components'; -import { Image } from '@api/room/type'; import { TextBox } from '@components/text-box'; import moment from 'moment'; +import { RESPONSE_CODE } from '@/constants/api'; export const InitRoomRegistration = () => { const [form] = Form.useForm(); @@ -102,14 +102,6 @@ export const InitRoomRegistration = () => { } }, []); - const getPrevImageFiles = () => { - const prevImageFile: Image[] = []; - for (let i = 0; i < imageFiles.length; i++) { - if (imageFiles[i].url) prevImageFile.push({ url: imageFiles[i].url }); - } - return prevImageFile; - }; - const { mutate: imageFile } = useImageFile({ onSuccess(data) { setUserInputValue((prevUserInputValueState) => { @@ -123,7 +115,23 @@ export const InitRoomRegistration = () => { const checkOutTime = form.getFieldValue('checkOutTime').format('HH:mm'); const count = form.getFieldValue('count'); - const prevImg = getPrevImageFiles(); + const newImages = []; + + const urls = data.data.urls; + for (let i = 0; i < imageFiles.length; i++) { + const image = imageFiles[i]; + if (image.url !== '') { + newImages.push({ url: image.url }); + } // 이미 이미지 url이 있는 상태 + } + + for (let i = 0; i < urls.length; i++) { + const url = urls[i].url; + if (typeof url === 'string') { + newImages.push({ url }); + } + } + const updatedRoom: Room = { name: roomName, price: price, @@ -133,7 +141,7 @@ export const InitRoomRegistration = () => { checkOutTime: checkOutTime, count: count, options: selectedOptions, - images: [...prevImg, ...(data.data.data.urls as unknown as Image[])], + images: newImages, }; const updatedRooms = [...prevUserInputValue.rooms]; @@ -155,6 +163,12 @@ export const InitRoomRegistration = () => { return [updatedUserInputValue]; }); + + setSelectedOptions({ airCondition: false, tv: false, internet: false }); + setImageFiles([]); + setSameRoomName(false); + setIsAddRoom(false); + navigate(ROUTES.INIT_INFO_CONFIRMATION); }, onError(error) { if (error instanceof AxiosError) { @@ -163,6 +177,12 @@ export const InitRoomRegistration = () => { style: { marginTop: '210px' }, }); } + if (error.response?.data.code === RESPONSE_CODE.IMAGE_SAVE_FAIL) { + message.error({ + content: '요청을 실패했습니다. 관리자에게 문의해주세요', + style: { marginTop: '210px' }, + }); + } }, }); @@ -187,12 +207,22 @@ export const InitRoomRegistration = () => { let shouldExecuteImageFile = false; - for (let index = 0; index < imageFiles.length; index++) { + for (let i = 0; i < imageFiles.length; i++) { + const image = imageFiles[i]; + if (image.file) shouldExecuteImageFile = true; + } + + for (let index = 0; index < 5; index++) { const image = imageFiles[index]; - if (image.file !== null) { - //사용자가 파일을 추가했을 때 - formData.append('image1', image.file); - shouldExecuteImageFile = true; + if (!image || image.file === null) { + // 등록한 적이 있거나 이미지 자체를 등록하지 않은 순서 + const emptyBlob = new Blob([], { type: 'application/octet-stream' }); + const nullFile = new File([emptyBlob], 'nullFile.txt', { + type: 'text/plain', + }); + formData.append(`image${index + 1}`, nullFile); + } else { + formData.append(`image${index + 1}`, image.file); } } @@ -219,9 +249,7 @@ export const InitRoomRegistration = () => { checkOutTime: checkOutTime, count: count, options: selectedOptions, - images: - userInputValue[0].rooms[userInputValue[0].editRoomIndex as number] - .images, + images: imageFiles, }; const updatedRooms = [...prevUserInputValue.rooms]; @@ -243,13 +271,13 @@ export const InitRoomRegistration = () => { return [updatedUserInputValue]; }); - } - setSelectedOptions({ airCondition: false, tv: false, internet: false }); - setImageFiles([]); - setSameRoomName(false); - setIsAddRoom(false); - navigate(ROUTES.INIT_INFO_CONFIRMATION); + setSelectedOptions({ airCondition: false, tv: false, internet: false }); + setImageFiles([]); + setSameRoomName(false); + setIsAddRoom(false); + navigate(ROUTES.INIT_INFO_CONFIRMATION); + } }; const areFormFieldsValid = () => { diff --git a/src/pages/sign-in/index.tsx b/src/pages/sign-in/index.tsx index 7a75fd82..8be5ce5c 100644 --- a/src/pages/sign-in/index.tsx +++ b/src/pages/sign-in/index.tsx @@ -21,9 +21,9 @@ export const SignIn = () => { const { accommodationListData } = useSideBar(); const { mutate } = usePostLogin({ onSuccess: (response) => { - setCookie('accessToken', response.data.data.accessToken); - setCookie('refreshToken', response.data.data.refreshToken); - const memberResponse = response.data.data.memberResponse; + setCookie('accessToken', response.data.accessToken); + setCookie('refreshToken', response.data.refreshToken); + const memberResponse = response.data.memberResponse; const memberData = JSON.stringify(memberResponse); localStorage.setItem('member', memberData); if (accommodationListData?.accommodations[0]?.id) { @@ -32,6 +32,63 @@ export const SignIn = () => { accommodationListData?.accommodations[0]?.id, ); } + try { + const res = isAccomodationList(); + if (res === true) { + const accomodationId = getCookie('accomodationId'); + setTimeout(() => { + handleChangeUrl(`/${accomodationId}/main`); + }, 1000); + } else { + setTimeout(() => { + handleChangeUrl('/init'); + }, 1000); + } + } catch (e) { + message.error({ + content: ( + + 요청에 실패했습니다. 잠시 후 다시 시도해 주세요. + + ), + duration: 2, + style: { + width: '346px', + height: '41px', + }, + }); + } + }, + onError(error) { + if (error instanceof AxiosError && error.response) { + if (error.response.status === HTTP_STATUS_CODE.BAD_GATEWAY) { + message.error({ + content: ( + + 요청에 실패했습니다. 잠시 후 다시 시도해 주세요. + + ), + duration: 2, + style: { + width: '346px', + height: '41px', + }, + }); + } else { + message.error({ + content: ( + + 이메일과 비밀번호를 확인해 주세요. + + ), + duration: 2, + style: { + width: '346px', + height: '41px', + }, + }); + } + } }, }); const isAccomodationList = () => { @@ -73,69 +130,11 @@ export const SignIn = () => { }, validationSchema: ValidateSchema, onSubmit: async (values) => { - try { - const signInData: SignInData = { - email: values.email, - password: values.password, - }; - await mutate(signInData); - try { - const res = isAccomodationList(); - if (res === true) { - const accomodationId = getCookie('accomodationId'); - setTimeout(() => { - handleChangeUrl(`/${accomodationId}/main`); - }, 1000); - } else { - setTimeout(() => { - handleChangeUrl('/init'); - }, 1000); - } - } catch (e) { - message.error({ - content: ( - - 요청에 실패했습니다. 잠시 후 다시 시도해 주세요. - - ), - duration: 2, - style: { - width: '346px', - height: '41px', - }, - }); - } - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status === HTTP_STATUS_CODE.BAD_GATEWAY) { - message.error({ - content: ( - - 요청에 실패했습니다. 잠시 후 다시 시도해 주세요. - - ), - duration: 2, - style: { - width: '346px', - height: '41px', - }, - }); - } else { - message.error({ - content: ( - - 이메일과 비밀번호를 확인해 주세요. - - ), - duration: 2, - style: { - width: '346px', - height: '41px', - }, - }); - } - } - } + const signInData: SignInData = { + email: values.email, + password: values.password, + }; + mutate(signInData); }, }); diff --git a/src/queries/init/index.ts b/src/queries/init/index.ts index 5832cc6b..745e3d8d 100644 --- a/src/queries/init/index.ts +++ b/src/queries/init/index.ts @@ -1,19 +1,23 @@ -import { PostImageFile } from '@api/init/type'; +import { + PostAccommodation, + PostAccommodationParams, + PostImageFile, +} from '@api/init/type'; import { UseMutationOptions, useMutation } from '@tanstack/react-query'; import { AxiosError, AxiosResponse } from 'axios'; -import { Response } from '@/types/api'; +import { ErrorResponse } from '@/types/api'; import { ACCOMMODATION_REGISTRATION_API } from '@api/init'; export const useImageFile = ( options?: UseMutationOptions< - AxiosResponse>, - AxiosError, + AxiosResponse, + AxiosError, FormData >, ) => { return useMutation< - AxiosResponse>, - AxiosError, + AxiosResponse, + AxiosError, FormData >( (formData: FormData) => @@ -24,4 +28,22 @@ export const useImageFile = ( ); }; -//숙소 등록 요청은 나중에 추가할 예정 +export const useAccommodationInfo = ( + options?: UseMutationOptions< + AxiosResponse, + AxiosError, + PostAccommodationParams + >, +) => { + return useMutation< + AxiosResponse, + AxiosError, + PostAccommodationParams + >( + (params: PostAccommodationParams) => + ACCOMMODATION_REGISTRATION_API.postAccommodationInfo(params), + { + ...options, + }, + ); +}; diff --git a/src/queries/sign-in/index.ts b/src/queries/sign-in/index.ts index 1073ef98..d48efcfb 100644 --- a/src/queries/sign-in/index.ts +++ b/src/queries/sign-in/index.ts @@ -1,21 +1,19 @@ import { SIGN_IN_API } from '@api/sign-in'; import { AxiosError, AxiosResponse } from 'axios'; import { UseMutationOptions, useMutation } from '@tanstack/react-query'; -import { Response } from '@/types/api'; import { MemberData, SignInData } from '@api/sign-in/type'; export const usePostLogin = ( options?: UseMutationOptions< - AxiosResponse>, + AxiosResponse, AxiosError, SignInData >, ) => { - return useMutation< - AxiosResponse>, - AxiosError, - SignInData - >((data: SignInData) => SIGN_IN_API.postLogin(data), { - ...options, - }); + return useMutation, AxiosError, SignInData>( + (data: SignInData) => SIGN_IN_API.postLogin(data), + { + ...options, + }, + ); }; diff --git a/src/stores/init/atoms.ts b/src/stores/init/atoms.ts index 2b61110a..b536501f 100644 --- a/src/stores/init/atoms.ts +++ b/src/stores/init/atoms.ts @@ -21,6 +21,7 @@ export const userInputValueState = atom({ detailAddress: '', zipCode: '', description: '', + thumbnail: '', type: '', images: [{ url: '' }], options: { diff --git a/src/test/sign-in/index.tsx b/src/test/sign-in/index.tsx index 12df6ac8..cdb00017 100644 --- a/src/test/sign-in/index.tsx +++ b/src/test/sign-in/index.tsx @@ -15,8 +15,8 @@ export const SignIn = () => { const { accommodationListData } = useSideBar(); const postLoginMutation = usePostLogin({ onSuccess: (response) => { - setCookie('accessToken', response.data.data.accessToken); - setCookie('refreshToken', response.data.data.accessToken); + setCookie('accessToken', response.data.accessToken); + setCookie('refreshToken', response.data.accessToken); }, });