From 642915d2680349a61a93fd164d37c5f5b343540c Mon Sep 17 00:00:00 2001 From: HyunJin <102955516+xxxjinn@users.noreply.github.com> Date: Sun, 21 Jan 2024 01:11:05 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=93=B1=EB=A1=9D=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=ED=95=98=EA=B8=B0=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20-=20=EA=B0=9D=EC=8B=A4,=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 89882 --- src/api/room/type.ts | 1 - src/components/init/ButtonContainer.tsx | 12 +- src/components/init/CheckBoxContainer.tsx | 7 + src/components/init/ImageUploadContainer.tsx | 93 ++++------ .../init-accommodation-registration/type.ts | 5 + .../init/init-info-confirmation/RoomItem.tsx | 36 +++- src/components/init/type.ts | 7 +- .../room/capacity-container/index.tsx | 1 - src/components/room/price-container/index.tsx | 1 - src/components/room/time-container/index.tsx | 21 ++- src/components/room/time-container/type.ts | 2 + .../init-accommodation-registration/index.tsx | 9 +- src/pages/init-info-confirmation/index.tsx | 23 ++- src/pages/init-room-registration/index.tsx | 173 +++++++++++++++--- src/stores/init/atoms.ts | 9 +- src/stores/init/type.ts | 5 + 16 files changed, 307 insertions(+), 98 deletions(-) create mode 100644 src/stores/init/type.ts diff --git a/src/api/room/type.ts b/src/api/room/type.ts index ac332301..f02ca35e 100644 --- a/src/api/room/type.ts +++ b/src/api/room/type.ts @@ -24,7 +24,6 @@ export type Options = { }; export type Image = { - id?: number; url: string; }; diff --git a/src/components/init/ButtonContainer.tsx b/src/components/init/ButtonContainer.tsx index 783806c6..64afe558 100644 --- a/src/components/init/ButtonContainer.tsx +++ b/src/components/init/ButtonContainer.tsx @@ -67,6 +67,10 @@ export const ButtonContainer = ({ }); }; + /* + StyledButton에 disabled를 임의로 true로 변경했습니다 + 추후 {!isValid} 로 바꿔주세요! + */ return ( {buttonStyle === 'navigate' && ( @@ -90,12 +94,18 @@ export const ButtonContainer = ({ size="large" onClick={confirm} data-testid="request-button" + disabled={!isValid} > 등록 요청 )} {buttonStyle === 'edit' && ( - + 수정하기 )} diff --git a/src/components/init/CheckBoxContainer.tsx b/src/components/init/CheckBoxContainer.tsx index 9b20d7d1..d0039eef 100644 --- a/src/components/init/CheckBoxContainer.tsx +++ b/src/components/init/CheckBoxContainer.tsx @@ -10,10 +10,12 @@ import { checkedRoomOptions, } from '@stores/init/atoms'; import { Options, RoomOptions } from './init-accommodation-registration/type'; +import { useEffect } from 'react'; export const CheckBoxContainer = ({ options, header, + defaultValue, }: CheckBoxContainerProps) => { const [selectedAccommodationOptions, setSelectedAccommodationOptions] = useRecoilState(checkedAccommodationOptions); @@ -21,6 +23,11 @@ export const CheckBoxContainer = ({ const [selectedInitRoomOptions, setSelectedInitRoomOptions] = useRecoilState(checkedRoomOptions); + useEffect(() => { + if (defaultValue) { + setSelectedInitRoomOptions(defaultValue); + } + }, [defaultValue]); const handleCheckboxChange = (event: CheckboxChangeEvent) => { const checkedOption = event.target.value; diff --git a/src/components/init/ImageUploadContainer.tsx b/src/components/init/ImageUploadContainer.tsx index 714a7986..6c1b7e05 100644 --- a/src/components/init/ImageUploadContainer.tsx +++ b/src/components/init/ImageUploadContainer.tsx @@ -1,23 +1,35 @@ import { TextBox } from '@components/text-box'; -import { Modal, message } from 'antd'; +import { message } from 'antd'; import { styled } from 'styled-components'; import { CloseCircleTwoTone, PlusOutlined } from '@ant-design/icons'; -import { useState, useRef, ChangeEvent } from 'react'; -import { ImageUploadFileItem, StyledImageContainerProps } from './type'; +import { useRef, ChangeEvent, useEffect } from 'react'; +import { StyledImageContainerProps } from './type'; import { IMAGE_MAX_CAPACITY, IMAGE_MAX_COUNT } from '@/constants/init'; import { colors } from '@/constants/colors'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { imageFileState } from '@stores/init/atoms'; import { ROUTES } from '@/constants/routes'; - -export const ImageUploadContainer = ({ header }: { header: string }) => { - const [previewOpen, setPreviewOpen] = useState(false); - const [previewTitle, setPreviewTitle] = useState(''); - const [fileList, setFileList] = useState([]); +import { Image } from '@api/room/type'; + +export const ImageUploadContainer = ({ + header, + images, +}: { + header: string; + images?: Image[]; +}) => { const fileInputRef = useRef(null); - const handleCancel = () => setPreviewOpen(false); - const setImageFile = useSetRecoilState(imageFileState); + useEffect(() => { + if (images) { + const data = images.map((image, index) => { + return { key: index, url: image.url, file: null }; + }); + setImageFile(data); + } + }, [images]); + + const [imageFile, setImageFile] = useRecoilState(imageFileState); const handleChange = (event: ChangeEvent) => { const inputElement = event.target; @@ -47,23 +59,10 @@ export const ImageUploadContainer = ({ header }: { header: string }) => { }); } if (selectedFile.size <= IMAGE_MAX_CAPACITY * 1024 * 1024) { - setImageFile((prev) => [...prev, selectedFile]); - - const reader = new FileReader(); - reader.onload = () => { - const imageUrl = reader.result as string; - - setFileList((prevFileList) => [ - ...prevFileList, - { - uid: Date.now(), - name: selectedFile.name, - url: imageUrl, - }, - ]); - }; - - reader.readAsDataURL(selectedFile); + setImageFile((prev) => [ + ...prev, + { key: imageFile.length, url: '', file: selectedFile }, + ]); } else { message.error({ content: `최대 ${IMAGE_MAX_CAPACITY}MB 파일 크기로 업로드 가능합니다.`, @@ -85,14 +84,9 @@ export const ImageUploadContainer = ({ header }: { header: string }) => { } }; - const handleImageClick = (file: ImageUploadFileItem) => { - setPreviewOpen(true); - setPreviewTitle(file.name); - }; - - const handleRemove = (file: ImageUploadFileItem) => { - const newFileList = fileList.filter((item) => item.uid !== file.uid); - setFileList(newFileList); + const handleRemove = (key: number) => { + const newFileList = imageFile.filter((item) => item.key !== key); + setImageFile(newFileList); }; return ( @@ -105,21 +99,20 @@ export const ImageUploadContainer = ({ header }: { header: string }) => { 이미지는 최대 {IMAGE_MAX_COUNT}개까지 등록 가능합니다. - - {fileList.map((file) => ( -
+ + {imageFile.map((obj) => ( +
handleRemove(file)} + onClick={() => handleRemove(obj.key)} twoToneColor={colors.black600} /> {file.name} handleImageClick(file)} + src={obj.file !== null ? URL.createObjectURL(obj.file) : obj.url} + alt={'이미지'} />
))} - {fileList.length < IMAGE_MAX_COUNT && ( + {imageFile.length < IMAGE_MAX_COUNT && ( @@ -137,18 +130,6 @@ export const ImageUploadContainer = ({ header }: { header: string }) => { )}
- - {previewTitle} file.name === previewTitle)?.url} - /> - ); }; diff --git a/src/components/init/init-accommodation-registration/type.ts b/src/components/init/init-accommodation-registration/type.ts index 7c4fc9c0..fe7ad66f 100644 --- a/src/components/init/init-accommodation-registration/type.ts +++ b/src/components/init/init-accommodation-registration/type.ts @@ -60,6 +60,10 @@ export type Room = { options: RoomOptions; }; +export type defaultRoom = { + images: Image[] | undefined; + options: RoomOptions | undefined; +}; export type onFinishValues = { 'room-name': string; price: string; @@ -80,4 +84,5 @@ export type UserInputValue = { images: Image[]; options: Options; rooms: Room[]; + editRoomIndex?: number | undefined; }; diff --git a/src/components/init/init-info-confirmation/RoomItem.tsx b/src/components/init/init-info-confirmation/RoomItem.tsx index 12f65aa0..77ecae78 100644 --- a/src/components/init/init-info-confirmation/RoomItem.tsx +++ b/src/components/init/init-info-confirmation/RoomItem.tsx @@ -7,12 +7,31 @@ import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { BsPeopleFill } from 'react-icons/bs'; import { ImageCarousel } from './ImageCarousel'; import { Room } from '../init-accommodation-registration/type'; -import { useRecoilState } from 'recoil'; -import { userInputValueState } from '@stores/init/atoms'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { + checkedRoomOptions, + imageFileState, + userInputValueState, +} from '@stores/init/atoms'; +import { useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; export const RoomItem = () => { const [userInputValue, setUserInputValue] = useRecoilState(userInputValueState); + const setImageFile = useSetRecoilState(imageFileState); + const setSelectedOptions = useSetRecoilState(checkedRoomOptions); + const navigate = useNavigate(); + + useEffect(() => { + setUserInputValue([{ ...userInputValue[0], editRoomIndex: -1 }]); + setImageFile([]); + setSelectedOptions({ + airCondition: false, + internet: false, + tv: false, + }); + }, []); const removeRoom = (room: Room) => { if (userInputValue[0].rooms.length === 1) { @@ -57,9 +76,14 @@ export const RoomItem = () => { }); }; + const roomEdit = (index: number) => { + setUserInputValue([{ ...userInputValue[0], editRoomIndex: index }]); + navigate('/init/room-registration'); + }; + return ( <> - {userInputValue[0].rooms.map((room: Room) => ( + {userInputValue[0].rooms.map((room: Room, index) => ( @@ -68,7 +92,11 @@ export const RoomItem = () => { {room.name} - } /> + } + onClick={() => roomEdit(index)} + /> } diff --git a/src/components/init/type.ts b/src/components/init/type.ts index fc65de3d..589c8f3a 100644 --- a/src/components/init/type.ts +++ b/src/components/init/type.ts @@ -1,4 +1,6 @@ import { FormInstance } from 'antd'; +import { RoomOptions } from './init-accommodation-registration/type'; +import { ImageFile } from '@stores/init/type'; export type ButtonContainerProps = { buttonStyle: 'navigate' | 'request' | 'edit'; @@ -12,17 +14,18 @@ export type ButtonContainerStyledWrapperProps = { export type CheckBoxContainerProps = { options: { [key: string]: string }; header: string; + defaultValue?: RoomOptions; }; export type ImageUploadFileItem = { uid: number; name: string; url: string; - originFileObj?: File; + originFileObj?: File | null; }; export type StyledImageContainerProps = { - $fileList: ImageUploadFileItem[]; + $fileList: ImageFile[]; header: string; }; diff --git a/src/components/room/capacity-container/index.tsx b/src/components/room/capacity-container/index.tsx index 013a2e26..515a8c68 100644 --- a/src/components/room/capacity-container/index.tsx +++ b/src/components/room/capacity-container/index.tsx @@ -22,7 +22,6 @@ export const CapacityContainer = ({ header, form }: CapacityContainerProps) => { setError('최대 인원은 기준 인원보다 작을 수 없습니다.'); } }, [defaultCapacity, maxCapacity]); - const validateInput = ({ value }: ValidateInputProps) => { setError(null); if (value < MIN_CAPACITY || value > MAX_CAPACITY) { diff --git a/src/components/room/price-container/index.tsx b/src/components/room/price-container/index.tsx index 329a7353..b7cd810f 100644 --- a/src/components/room/price-container/index.tsx +++ b/src/components/room/price-container/index.tsx @@ -51,7 +51,6 @@ export const PriceContainer = ({ header, form }: PriceContainerProps) => { { +export const TimeContainer = ({ + header, + form, + defaultCheckInTime, + defaultCheckOutTime, +}: TimeContainerProps) => { const format = 'HH:mm'; const specificTime = moment().hours(9).minutes(0); const [checkInTime, setCheckInTime] = useState(specificTime); @@ -47,7 +52,12 @@ export const TimeContainer = ({ header, form }: TimeContainerProps) => { 체크인 - + { 체크아웃 - + { const isEdit = useRecoilValue(accommodationEditState); const [imageFiles, setImageFiles] = useRecoilState(imageFileState); + const setUpdatedAccommodationInfo = useSetRecoilState( + isUpdatedAccommodationState, + ); const accommodationOptions = { cooking: '객실취사', @@ -109,10 +113,11 @@ export const InitAccommodationRegistration = () => { const formData = new FormData(); imageFiles.forEach((image) => { - formData.append('image1', image); + if (image.file !== null) formData.append('image1', image.file); }); imageFile(formData); + setUpdatedAccommodationInfo(true); navigate(ROUTES.INIT_ROOM_REGISTRATION); }; diff --git a/src/pages/init-info-confirmation/index.tsx b/src/pages/init-info-confirmation/index.tsx index 5c1cf773..38af8255 100644 --- a/src/pages/init-info-confirmation/index.tsx +++ b/src/pages/init-info-confirmation/index.tsx @@ -4,14 +4,28 @@ 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 { + 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'; import styled from 'styled-components'; export const InitInfoConfirmation = () => { - const userInput = window.localStorage.getItem('userInput'); + const userInputValue = useRecoilValue(userInputValueState); const navigate = useNavigate(); - if (!userInput) { + const setUpdatedAccommodationInfo = useSetRecoilState( + isUpdatedAccommodationState, + ); + + useEffect(() => { + setUpdatedAccommodationInfo(true); + }, []); + + if (userInputValue[0].name === '') { return ( @@ -33,7 +47,10 @@ export const InitInfoConfirmation = () => { - + ); }; diff --git a/src/pages/init-room-registration/index.tsx b/src/pages/init-room-registration/index.tsx index c078aef5..51489752 100644 --- a/src/pages/init-room-registration/index.tsx +++ b/src/pages/init-room-registration/index.tsx @@ -6,7 +6,7 @@ import { ImageUploadContainer } from '@components/init/ImageUploadContainer'; import { NameContainer } from '@components/init/NameContainer'; import { Room, - onFinishValues, + defaultRoom, } from '@components/init/init-accommodation-registration/type'; import { CapacityContainer } from '@components/room/capacity-container'; import { CountContainer } from '@components/room/num-of-rooms-container'; @@ -16,6 +16,7 @@ import { useImageFile } from '@queries/init'; import { checkedRoomOptions, imageFileState, + isUpdatedAccommodationState, userInputValueState, } from '@stores/init/atoms'; import { capacityHasError, priceHasError } from '@stores/room/atoms'; @@ -27,6 +28,7 @@ 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'; export const InitRoomRegistration = () => { const [form] = Form.useForm(); @@ -38,26 +40,77 @@ export const InitRoomRegistration = () => { }; const navigate = useNavigate(); - const [isValid, setIsValid] = useState(false); const [userInputValue, setUserInputValue] = useRecoilState(userInputValueState); + const [isValid, setIsValid] = useState( + userInputValue[0].editRoomIndex !== -1, + ); const [selectedOptions, setSelectedOptions] = useRecoilState(checkedRoomOptions); const [imageFiles, setImageFiles] = useRecoilState(imageFileState); const [sameRoomName, setSameRoomName] = useState(false); + + const [defaultValue, setDefaultValue] = useState({ + images: undefined, + options: undefined, + }); + const priceError = useRecoilValue(priceHasError); const capacityError = useRecoilValue(capacityHasError); - const userInput = window.localStorage.getItem('userInput'); + const isUpdatedAccommodation = useRecoilValue(isUpdatedAccommodationState); + + useEffect(() => { + if ( + userInputValue[0].editRoomIndex !== undefined && + userInputValue[0].editRoomIndex !== -1 + ) { + const index = userInputValue[0].editRoomIndex; + form.setFieldValue('room-name', userInputValue[0].rooms[index].name); + form.setFieldValue( + 'price', + userInputValue[0].rooms[index].price?.toLocaleString(), + ); + form.setFieldValue( + 'defaultCapacity', + userInputValue[0].rooms[index].defaultCapacity, + ); + form.setFieldValue( + 'maxCapacity', + userInputValue[0].rooms[index].maxCapacity, + ); + form.setFieldValue('count', userInputValue[0].rooms[index].count); + form.setFieldValue( + 'checkInTime', + moment(userInputValue[0].rooms[index].checkInTime, 'HH:mm'), + ); + form.setFieldValue( + 'checkOutTime', + moment(userInputValue[0].rooms[index].checkOutTime, 'HH:mm'), + ); + setDefaultValue({ + images: userInputValue[0].rooms[index].images, + options: userInputValue[0].rooms[index].options, + }); + } + }, []); + + const getPrevImageFiles = () => { + const prevImageFile: Image[] = []; + for (let i = 0; i < imageFiles.length; i++) { + prevImageFile.push({ url: imageFiles[i].url }); + } + return prevImageFile; + }; const { mutate: imageFile } = useImageFile({ onSuccess(data) { setUserInputValue((prevUserInputValueState) => { - const [userInputValue] = prevUserInputValueState; + const [prevUserInputValue] = prevUserInputValueState; const roomName = form.getFieldValue('room-name'); const price = parseInt(form.getFieldValue('price').replace(',', '')); @@ -67,6 +120,7 @@ export const InitRoomRegistration = () => { const checkOutTime = form.getFieldValue('checkOutTime').format('HH:mm'); const count = form.getFieldValue('count'); + const prevImg = getPrevImageFiles(); const updatedRoom: Room = { name: roomName, price: price, @@ -76,19 +130,28 @@ export const InitRoomRegistration = () => { checkOutTime: checkOutTime, count: count, options: selectedOptions, - images: data.data.data.urls as unknown as Image[], + images: [...prevImg, ...(data.data.data.urls as unknown as Image[])], }; + const updatedRooms = [...prevUserInputValue.rooms]; + + if ( + userInputValue[0].editRoomIndex !== undefined && + userInputValue[0].editRoomIndex !== -1 + ) { + updatedRooms[userInputValue[0].editRoomIndex] = updatedRoom; + } else { + updatedRooms.push(updatedRoom); + } + const updatedUserInputValue = { - ...userInputValue, - rooms: [...userInputValue.rooms, updatedRoom], + ...prevUserInputValue, + rooms: updatedRooms, + editRoomIndex: -1, }; return [updatedUserInputValue]; }); - setSelectedOptions({ airCondition: false, tv: false, internet: false }); - setImageFiles([]); - navigate(ROUTES.INIT_INFO_CONFIRMATION); }, onError(error) { if (error instanceof AxiosError) { @@ -100,14 +163,14 @@ export const InitRoomRegistration = () => { }, }); - const onFinish = (values: onFinishValues) => { + const onFinish = () => { const roomsArray = userInputValue[0].rooms; - + const roomNameValue = form.getFieldValue('room-name'); const hasDuplicate = roomsArray.some( - (room: Room) => room.name === values['room-name'], + (room: Room) => room.name === roomNameValue, ); - if (hasDuplicate) { + if (hasDuplicate && userInputValue[0].editRoomIndex == -1) { setSameRoomName(true); message.error({ content: '동일한 객실명의 상품이 이미 존재합니다.', @@ -119,13 +182,68 @@ export const InitRoomRegistration = () => { const formData = new FormData(); - imageFiles.forEach((image) => { - formData.append('image1', image); - }); + let shouldExecuteImageFile = false; + + for (let index = 0; index < imageFiles.length; index++) { + const image = imageFiles[index]; + if (image.file !== null) { + //사용자가 파일을 추가했을 때 + formData.append('image1', image.file); + shouldExecuteImageFile = true; + } + } + + if (shouldExecuteImageFile) { + imageFile(formData); //그 파일에 대한 url을 가져와서, setUserInputValue까지 한번에 해줌. + } else { + setUserInputValue((prevUserInputValueState) => { + const [prevUserInputValue] = prevUserInputValueState; + + const roomName = form.getFieldValue('room-name'); + const price = parseInt(form.getFieldValue('price').replace(',', '')); + const defaultCapacity = form.getFieldValue('defaultCapacity'); + const maxCapacity = form.getFieldValue('maxCapacity'); + const checkInTime = form.getFieldValue('checkInTime').format('HH:mm'); + const checkOutTime = form.getFieldValue('checkOutTime').format('HH:mm'); + const count = form.getFieldValue('count'); + + const updatedRoom: Room = { + name: roomName, + price: price, + defaultCapacity: defaultCapacity, + maxCapacity: maxCapacity, + checkInTime: checkInTime, + checkOutTime: checkOutTime, + count: count, + options: selectedOptions, + images: userInputValue[0].images, + }; + + const updatedRooms = [...prevUserInputValue.rooms]; + + if ( + userInputValue[0].editRoomIndex !== undefined && + userInputValue[0].editRoomIndex !== -1 + ) { + updatedRooms[userInputValue[0].editRoomIndex] = updatedRoom; + } else { + updatedRooms.push(updatedRoom); + } + + const updatedUserInputValue = { + ...prevUserInputValue, + rooms: updatedRooms, + editRoomIndex: -1, + }; - imageFile(formData); + return [updatedUserInputValue]; + }); + } + setSelectedOptions({ airCondition: false, tv: false, internet: false }); + setImageFiles([]); setSameRoomName(false); + navigate(ROUTES.INIT_INFO_CONFIRMATION); }; const areFormFieldsValid = () => { @@ -148,7 +266,7 @@ export const InitRoomRegistration = () => { useEffect(() => { setIsValid(areFormFieldsValid()); - }, [selectedOptions, priceHasError, capacityError]); + }, [imageFiles, priceHasError, capacityError]); const handleFormValuesChange = () => { setIsValid(areFormFieldsValid()); @@ -168,15 +286,24 @@ export const InitRoomRegistration = () => { isSameRoomName={sameRoomName} /> - + - - + + ({ seminar: false, }, rooms: [], + editRoomIndex: -1, }, ], effects_UNSTABLE: [persistAtom], @@ -67,7 +69,12 @@ export const accommodationEditState = atom({ default: false, }); -export const imageFileState = atom({ +export const imageFileState = atom({ key: 'imageFileState', default: [], }); + +export const isUpdatedAccommodationState = atom({ + key: 'isUpdatedAccommodation', + default: false, +}); diff --git a/src/stores/init/type.ts b/src/stores/init/type.ts new file mode 100644 index 00000000..2d6aa70b --- /dev/null +++ b/src/stores/init/type.ts @@ -0,0 +1,5 @@ +export type ImageFile = { + key: number; + file: File | null; + url: string; +};