-
Notifications
You must be signed in to change notification settings - Fork 3
Host_Manage
SeongWoo Shin edited this page Aug 7, 2023
·
10 revisions
- REST API 통신(delete)으로 숙소 데이터 삭제 요청
- useMediaQuery 커스텀 훅으로 브라우저 너비에 따른 스타일 변경
AccommodationFilter.tsx
interface StyledButtonProps {
changePoint: boolean;
}
interface AccommodationDeleteButtonProps {
houseId: string | undefined;
}
export default function AccommodationDeleteButton({ houseId }: AccommodationDeleteButtonProps) {
const changePoint = useMediaQuery('(min-width: 780px)');
const navigate = useNavigate();
const [openModal, setOpenModal] = useState<boolean>(false);
const { sendRequest, responseData } = useAuthorizedRequest<any>({});
const openCloseDeleteCheckModal = () => {
setOpenModal(preState => !preState);
};
const deleteAccommodation = async () => {
try {
await sendRequest({ url: `/user/houses/${houseId}`, method: 'DELETE' });
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (!responseData) return;
if (responseData.isSuccess) {
alert('숙소 삭제가 완료되었습니다.');
navigate('/hosting');
} else {
alert('숙소 삭제가 완료되지않았습니다.');
}
}, [responseData]);
return (
<>
<StyledButton changePoint={changePoint} onClick={openCloseDeleteCheckModal}>
<RiDeleteBinLine size={20} />
{changePoint && <>숙소 삭제하기</>}
</StyledButton>
{openModal && (
<DeleteCheckModal
label="숙소를 삭제하시겠습니까?"
handleOnButton={deleteAccommodation}
onClose={openCloseDeleteCheckModal}
/>
)}
</>
);
}
const StyledButton = styled.button<StyledButtonProps>`
display: flex;
align-items: center;
justify-content: center;
width: ${({ changePoint }) => (changePoint ? '130px' : '50px')};
height: 50px;
padding: 10px;
border-radius: ${({ changePoint }) => (changePoint ? '10px' : '50%')};
gap: 10px;
border: 1px solid black;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
&:active {
transform: scale(0.9);
transition: transform 200ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
`;
useMediaQuery.ts
import { useEffect, useState } from 'react';
/**@param query CSS 미디어쿼리 ex) "(min-width: 780px)" */
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const updateMatches = () => {
setMatches(mediaQuery.matches);
};
updateMatches();
mediaQuery.addEventListener('change', updateMatches);
return () => {
mediaQuery.removeEventListener('change', updateMatches);
};
}, [query]);
return matches;
}
-
스크롤 메뉴 - useRef hook과 window.scrollTo()를 활용해서 해당 메뉴로 이동 구현
-
사진, 숙소 정보(이름, 타입, 설명), 편의시설, 위치, 객실 각 항목 수정 기능 (REST API - PATCH 요청)
- 사진 - 업로드, 변경, 삭제 (별도 옵션 모달을 통해 구현)
- 숙소 정보 - 타입 라디오박스 역할의 버튼 생성, 이름과 설명 textarea onChange 이벤트 호출 시, 잦은 상태 변화를 막기위해 debounce 적용
- 편의시설 - 체크박스 버튼 형식 구현
- 위치 - 입력한 주소로 부터 위도, 경도 얻는 유틸 함수 구현 (구글 API geocode 객체 메서드 활용)
- 객실 - 사진 업로드 및 삭제, 페이지네이션
HostingManageMain.tsx
export default function HostingManageMain() {
const { houseId } = useParams();
const { sendRequest, responseData } = useAuthorizedRequest<any>({});
const photoRef = useRef<HTMLDivElement>(null);
const basicInfoRef = useRef<HTMLDivElement>(null);
const amenityRef = useRef<HTMLDivElement>(null);
const locationRef = useRef<HTMLDivElement>(null);
const roomRef = useRef<HTMLDivElement>(null);
const [scrollEventActive, setScrollEventActive] = useState<boolean>(true);
const [originalAccommodationData, setOriginalAccommodationData] = useRecoilState(originalAccommodationDataState);
const [originalRoomData, setOriginalRoomData] = useRecoilState(originalRoomDataState);
const [originalPatchHouseReq, setOriginalPatchHouseReq] = useRecoilState(originalPatchHouseReqState);
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [roomData, setRoomData] = useRecoilState(roomDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const [selectedItemIndex, setSelectedItemIndex] = useState<number>(0);
const scrollToElement = (ref: React.RefObject<HTMLDivElement>) => {
if (ref.current) {
setScrollEventActive(false);
window.scrollTo({
top: ref.current.offsetTop,
behavior: 'smooth',
});
setTimeout(() => {
setScrollEventActive(true);
}, 1000);
}
};
useEffect(() => {
const handleScroll = () => {
if (!scrollEventActive) return;
const scrollPositionTop = document.documentElement.scrollTop || document.body.scrollTop;
if (photoRef.current && scrollPositionTop >= photoRef.current.offsetTop) {
setSelectedItemIndex(0);
}
if (basicInfoRef.current && scrollPositionTop >= basicInfoRef.current.offsetTop) {
setSelectedItemIndex(1);
}
if (amenityRef.current && scrollPositionTop >= amenityRef.current.offsetTop) {
setSelectedItemIndex(2);
}
if (locationRef.current && scrollPositionTop >= locationRef.current.offsetTop) {
setSelectedItemIndex(3);
}
if (roomRef.current && scrollPositionTop >= roomRef.current.offsetTop) {
setSelectedItemIndex(4);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [scrollEventActive]);
useEffect(() => {
const fetchData = async () => {
await sendRequest({ url: `/api/houses/${houseId}`, method: 'POST' });
};
fetchData();
}, []);
useEffect(() => {
if (!responseData) return;
if (responseData.isSuccess) {
setAccommodationData(responseData.result.house);
setRoomData(responseData.result.rooms);
setOriginalAccommodationData(responseData.result.house);
setOriginalRoomData(responseData.result.rooms);
const { name, type, options, contents, postCode, sido, sigungu, fullAddress, lat, lng }: AccommodationData =
responseData.result.house;
const option = { wifi: false, pc: false, parking: false, bbq: false };
if (options.length > 0) {
options.forEach((v, i) => (option[v] = true));
}
const newPatchData = {
name,
type,
option,
contents,
address: { postCode, sido, sigungu, fullAddress, lat, lng },
patchImageReqs: [],
};
setPatchHouseReq(newPatchData);
setOriginalPatchHouseReq(newPatchData);
}
}, [responseData]);
return (
<StyledFlexDiv>
<StyledManageContainer>
<StyledTitleContainer>
<StyledName>{accommodationData?.name}</StyledName>
<AccommodationDeleteButton houseId={houseId}></AccommodationDeleteButton>
</StyledTitleContainer>
<StyledContentsContainer>
<StyledMenu>
<StyledSubName>숙소 기본 정보</StyledSubName>
<StyledNav>
<StyledUl>
{[
['사진', photoRef],
['숙소 기본 정보', basicInfoRef],
['편의시설', amenityRef],
['위치', locationRef],
['객실', roomRef],
].map((item, index) => (
<StyledLi
key={index}
onClick={() => {
setSelectedItemIndex(index);
scrollToElement(item[1] as React.RefObject<HTMLDivElement>);
}}
>
<>{item[0]}</>
</StyledLi>
))}
</StyledUl>
<StyledMoveBar selectedIndex={selectedItemIndex} />
</StyledNav>
</StyledMenu>
<StyledInfo>
<AccommodationInfoItem
houseId={houseId}
editContents={<AccommodationEditPicture images={accommodationData?.houseImages as string[]} />}
Ref={photoRef}
label="사진"
>
<AccommodationPicture />
</AccommodationInfoItem>
<AccommodationInfoItem
houseId={houseId}
editContents={<AccommodationEditContents />}
Ref={basicInfoRef}
label="숙소기본정보"
>
<AccommodationContents />
</AccommodationInfoItem>
<AccommodationInfoItem
houseId={houseId}
editContents={<AccommodationEditAmenity />}
Ref={amenityRef}
label="편의시설"
>
<AccommodationAmenity />
</AccommodationInfoItem>
<AccommodationInfoItem
houseId={houseId}
editContents={<AddressInputContents />}
Ref={locationRef}
label="위치"
>
{accommodationData?.fullAddress}
</AccommodationInfoItem>
<RoomInfoItem houseId={houseId} editContents={<RoomEditInfo />} Ref={roomRef} label="객실">
<RoomInfo />
</RoomInfoItem>
</StyledInfo>
</StyledContentsContainer>
</StyledManageContainer>
</StyledFlexDiv>
);
}
AccommodationInfoItem.tsx
export default function AccommodationInfoItem({
Ref,
label,
children,
editContents,
houseId,
}: AccommodationInfoItemProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { sendRequest, responseData } = useAuthorizedRequest<any>({});
const { sendRequest: editedSendRequest, responseData: editedResponseData } = useAuthorizedRequest<any>({});
const [originalAccommodationData, setOriginalAccommodationData] = useRecoilState(originalAccommodationDataState);
const [originalRoomData, setOriginalRoomData] = useRecoilState(originalRoomDataState);
const [originalPatchHouseReq, setOriginalPatchHouseReq] = useRecoilState(originalPatchHouseReqState);
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [roomData, setRoomData] = useRecoilState(roomDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const formData = useRecoilValue(formDataState);
const openCloseEditComponent = () => {
setAccommodationData(originalAccommodationData);
setRoomData(originalRoomData);
setPatchHouseReq(originalPatchHouseReq);
setIsOpen(preState => !preState);
};
const dataPatch = async () => {
const newFormData = new FormData();
formData.forEach((value, key) => {
newFormData.append(key, value);
});
if (accommodationData.houseImages.length < 5) {
alert('숙소 사진을 5장 등록해주세요.');
return;
}
newFormData.delete('patchHouseReq');
newFormData.append('patchHouseReq', new Blob([JSON.stringify(patchHouseReq)], { type: 'application/json' }));
try {
await sendRequest({ url: `/user/houses/${houseId}`, method: 'PATCH', body: newFormData });
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (!responseData) return;
const fetchData = async () => {
try {
await editedSendRequest({ url: `/api/houses/${houseId}`, method: 'POST' });
} catch (err) {
console.log(err);
}
};
if (responseData.isSuccess) {
fetchData();
alert('숙소 수정이 완료되었습니다.');
setOriginalAccommodationData(accommodationData);
setIsOpen(preState => !preState);
} else {
alert('숙소 수정을 하지못했습니다.');
}
}, [responseData]);
useEffect(() => {
if (!editedResponseData) return;
if (editedResponseData.isSuccess) {
setAccommodationData(editedResponseData.result.house);
setRoomData(editedResponseData.result.rooms);
setOriginalAccommodationData(editedResponseData.result.house);
setOriginalRoomData(editedResponseData.result.rooms);
const { name, type, options, contents, postCode, sido, sigungu, fullAddress, lat, lng }: AccommodationData =
editedResponseData.result.house;
const option = { wifi: false, pc: false, parking: false, bbq: false };
if (options.length > 0) {
options.forEach((v, i) => (option[v] = true));
}
const newPatchData = {
name,
type,
option,
contents,
address: { postCode, sido, sigungu, fullAddress, lat, lng },
patchImageReqs: [],
};
setPatchHouseReq(newPatchData);
setOriginalPatchHouseReq(newPatchData);
}
}, [editedResponseData]);
return (
<StyledItemContainer ref={Ref}>
<StyledItemHeader>
<StyledLabel>{label}</StyledLabel>
<StyledEdit onClick={openCloseEditComponent}>수정</StyledEdit>
</StyledItemHeader>
{isOpen ? (
<StyledEditContainer>
<StyledEditHeader>
<div />
<StyledCloseIcon onClick={openCloseEditComponent} />
</StyledEditHeader>
<>{editContents}</>
<StyledEditFooter>
<StyledCancelButton onClick={openCloseEditComponent}>취소</StyledCancelButton>
<StyledEditButton onClick={dataPatch}>저장하기</StyledEditButton>
</StyledEditFooter>
</StyledEditContainer>
) : (
<>{children}</>
)}
</StyledItemContainer>
);
}
AccommodationEditPicture.tsx
interface AccommodationEditPictureProps {
images: Array<string>;
}
export default function AccommodationEditPicture({ images }: AccommodationEditPictureProps) {
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const [formData, setFormData] = useRecoilState(formDataState);
const inputRef = useRef<HTMLInputElement>(null);
const addPicture = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files;
if (file?.[0]) {
const imageDataValues = Array.from(formData.getAll('houseImages'));
const newFormData = new FormData();
formData.forEach((value, key) => {
newFormData.append(key, value);
});
try {
const result = (await imageReader(file[0])) as string;
const newAccommodationData = { ...accommodationData };
newAccommodationData.houseImages = [...newAccommodationData.houseImages];
const newPatchHouseReq = { ...patchHouseReq };
newPatchHouseReq.patchImageReqs = [...newPatchHouseReq.patchImageReqs];
newAccommodationData.houseImages.push(result);
imageDataValues.push(file[0]);
newPatchHouseReq.patchImageReqs.push({
imageCount: newAccommodationData.houseImages.length,
imageStatus: 'ADD',
});
newFormData.delete('houseImages');
imageDataValues.forEach(value => newFormData.append('houseImages', value));
setFormData(newFormData);
setAccommodationData(newAccommodationData);
setPatchHouseReq(newPatchHouseReq);
} catch (err) {
console.log(err);
}
}
};
const resetFileInput = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
};
return (
<StyledGridDiv>
{images.map((image, idx) => {
return (
<StyledContainer key={idx}>
<ImageOption index={idx} />
<StyledImage src={image} alt="이미지" />
</StyledContainer>
);
})}
{images.length < 5 && (
<StyledContainer>
<StyledLastImgIcon />
<StyledPlusLabel htmlFor="fileLast"></StyledPlusLabel>
<StyledPlusInput
ref={inputRef}
id="fileLast"
type="file"
accept="image/*"
onChange={addPicture}
onClick={resetFileInput}
></StyledPlusInput>
<StyledLastContentSubTitle>추가</StyledLastContentSubTitle>
</StyledContainer>
)}
</StyledGridDiv>
);
}
imageReader.ts
const imageReader = (file: Blob | null) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
if (file) fileReader.readAsDataURL(file);
fileReader.onload = () => {
resolve(fileReader.result);
};
fileReader.onerror = error => {
reject(error);
};
});
};
export default imageReader;
ImageOption.tsx
interface ImageOptionProps {
index: number;
}
export default function ImageOption({ index }: ImageOptionProps) {
const [isClicked, setIsClicked] = useState<boolean>(false);
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const [formData, setFormData] = useRecoilState(formDataState);
const divRef = useRef<HTMLDivElement>(null);
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsClicked(preState => !preState);
};
const deleteImage = (index: number) => () => {
setIsClicked(false);
const newAccommodationData = { ...accommodationData };
newAccommodationData.houseImages = [...newAccommodationData.houseImages];
const newPatchHouseReq = { ...patchHouseReq };
newPatchHouseReq.patchImageReqs = [...newPatchHouseReq.patchImageReqs];
newAccommodationData.houseImages.splice(index, 1);
newPatchHouseReq.patchImageReqs.push({ imageCount: index, imageStatus: 'DELETE' });
const newFormData = new FormData();
formData.forEach((value, key) => {
newFormData.append(key, value);
});
newFormData.delete('houseImages');
setFormData(newFormData);
setAccommodationData(newAccommodationData);
setPatchHouseReq(newPatchHouseReq);
setIsClicked(false);
};
const editImage = (index: number) => async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files;
setIsClicked(true);
if (file?.[0]) {
const imageDataValues = Array.from(formData.getAll('houseImages'));
const newFormData = new FormData();
formData.forEach((value, key) => {
newFormData.append(key, value);
});
try {
const result = (await imageReader(file[0])) as string;
const newAccommodationData = { ...accommodationData };
newAccommodationData.houseImages = [...newAccommodationData.houseImages];
const newPatchHouseReq = { ...patchHouseReq };
newPatchHouseReq.patchImageReqs = [...newPatchHouseReq.patchImageReqs];
newAccommodationData.houseImages.splice(index, 1, result);
imageDataValues.splice(index, 1, file[0]);
newPatchHouseReq.patchImageReqs.push({ imageCount: index, imageStatus: 'MODIFY' });
newFormData.delete('houseImages');
imageDataValues.forEach(value => newFormData.append('houseImages', value));
setFormData(newFormData);
setAccommodationData(newAccommodationData);
setPatchHouseReq(newPatchHouseReq);
setIsClicked(false);
} catch (err) {
console.log(err);
}
}
};
useEffect(() => {
const handleClickOutside = (e: Event) => {
if (divRef.current && !divRef.current.contains(e.target as Node)) {
setIsClicked(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, []);
return (
<StyledRelativeDiv ref={divRef}>
<StyledBtn onMouseDown={handleOnClick}>
<SlOptions />
</StyledBtn>
{isClicked && (
<StyledOptionContainer>
<StyledOption>
<StyleLabel htmlFor="editFile">수정하기</StyleLabel>
<StyledInput onChange={editImage(index)} id="editFile" type="file" accept="image/*" />
</StyledOption>
<StyledOption onClick={deleteImage(index)}>삭제</StyledOption>
</StyledOptionContainer>
)}
</StyledRelativeDiv>
);
}
AccommodationEditContents.tsx
export default function AccommodationEditContents() {
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const typeList: (keyof AccommodationType)[] = ['MOTEL', 'HOTEL', 'PENSION', 'GUEST'];
const [selectedType, setSelectedType] = useState<keyof AccommodationType>();
const changeAccommodationType = (e: React.MouseEvent<HTMLButtonElement>) => {
const value = (e.target as HTMLInputElement).value as keyof AccommodationType;
setSelectedType(value);
const newAccommodationData = { ...accommodationData, type: value };
setAccommodationData(newAccommodationData);
const newPatchHouseReq = { ...patchHouseReq, type: value };
setPatchHouseReq(newPatchHouseReq);
};
const changeText = debounce((e: ChangeEvent<HTMLTextAreaElement>) => {
if (e.target.name === 'name') {
const newAccommodation = { ...accommodationData, name: e.target.value.replace(/\r\n|\r|\n/g, ' ') };
setAccommodationData(newAccommodation);
const newPatchHouseReq = { ...patchHouseReq, name: e.target.value.replace(/\r\n|\r|\n/g, ' ') };
setPatchHouseReq(newPatchHouseReq);
}
if (e.target.name === 'contents') {
const newAccommodation = { ...accommodationData, contents: e.target.value };
setAccommodationData(newAccommodation);
const newPatchHouseReq = { ...patchHouseReq, contents: e.target.value };
setPatchHouseReq(newPatchHouseReq);
}
}, 10);
return (
<StyledEditContentsContainer>
<StyledEditContent>
<StyledContentsTitle>타입</StyledContentsTitle>
<StyledTypeButtonContainer>
{typeList.map((type, idx) => {
return (
<StyledButtonDiv key={idx}>
<StyledItemButton
value={type}
type="button"
role="checkbox"
aria-checked={type === selectedType}
onClick={changeAccommodationType}
>
{AccommodationIconMap[type]}
<StyledItemName>{AccommodationNameMap[type]}</StyledItemName>
</StyledItemButton>
</StyledButtonDiv>
);
})}
</StyledTypeButtonContainer>
</StyledEditContent>
<StyledEditContent>
<StyledContentsTitle>숙소이름</StyledContentsTitle>
<StyledTextBox name="name" defaultValue={accommodationData.name} onChange={changeText}></StyledTextBox>
</StyledEditContent>
<StyledEditContent>
<StyledContentsTitle>숙소설명</StyledContentsTitle>
<StyledTextBox name="contents" defaultValue={accommodationData.contents} onChange={changeText}></StyledTextBox>
</StyledEditContent>
</StyledEditContentsContainer>
);
}
AccommodationEditAmenity.tsx
export default function AccommodationEditAmenity() {
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const amenityList: (keyof AmenityType)[] = ['pc', 'wifi', 'parking', 'bbq'];
const changeAmenity = (e: React.MouseEvent<HTMLButtonElement>) => {
const value = (e.target as HTMLInputElement).value as keyof AmenityType;
const newAccommodationData = { ...accommodationData };
newAccommodationData.options = [...newAccommodationData.options];
if (newAccommodationData.options.includes(value)) {
const index = newAccommodationData.options.indexOf(value);
newAccommodationData.options.splice(index, 1);
} else {
newAccommodationData.options.push(value);
}
const newPatchHouseReq = { ...patchHouseReq };
newPatchHouseReq.option = { ...newPatchHouseReq.option };
newPatchHouseReq.option[value] = !newPatchHouseReq.option[value];
setAccommodationData(newAccommodationData);
setPatchHouseReq(newPatchHouseReq);
};
return (
<StyledAmenityContainer>
{amenityList.map((amenity, idx) => {
return (
<StyledAmenityDiv key={idx}>
<StyledItemButton
value={amenity}
type="button"
role="checkbox"
aria-checked={accommodationData.options.includes(amenity)}
onClick={changeAmenity}
>
{AmenityIconMap[amenity]}
<StyledTextContainer>
<StyledItemName>{AmenityNameMap[amenity]}</StyledItemName>
</StyledTextContainer>
</StyledItemButton>
</StyledAmenityDiv>
);
})}
</StyledAmenityContainer>
);
}
AccommodationEditAddress.tsx
export default function AddressInputContents() {
const [accommodationData, setAccommodationData] = useRecoilState(accommodationDataState);
const [patchHouseReq, setPatchHouseReq] = useRecoilState(patchHouseReqState);
const [isFocused, setIsFocused] = useState<FocusList>({
sido: false,
sigungu: false,
fullAddress: false,
postCode: false,
});
const handleInputFocus = (e: FocusEvent<HTMLInputElement>) => {
const newIsFocused = { ...isFocused };
newIsFocused[e.target.id] = true;
setIsFocused(newIsFocused);
};
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
const newIsBlur = { ...isFocused };
if (!e.target.value) {
newIsBlur[e.target.id] = false;
setIsFocused(newIsBlur);
}
};
const handleOnChangeAddress = async (e: ChangeEvent<HTMLInputElement>) => {
const updatedField = e.target.id;
const updatedValue = e.target.value;
setAccommodationData(preAccommodationData => ({
...preAccommodationData,
[updatedField]: updatedValue,
}));
setPatchHouseReq(prePatchHouseReq => ({
...prePatchHouseReq,
address: {
...prePatchHouseReq.address,
[updatedField]: updatedValue,
},
}));
if (updatedField === 'fullAddress') {
try {
const res = await getLatLngFromAddress(updatedField);
setPatchHouseReq(prevPatchHouseReq => ({
...prevPatchHouseReq,
address: {
...prevPatchHouseReq.address,
lat: res?.lat as number,
lng: res?.lng as number,
},
}));
setAccommodationData(preAccommodationData => ({
...preAccommodationData,
lat: res?.lat as number,
lng: res?.lng as number,
}));
} catch (err) {
console.log(err);
}
}
};
useEffect(() => {
const checkInitialValue = () => {
const newIsFocused = { ...isFocused };
if (accommodationData.sido) newIsFocused.sido = true;
if (accommodationData.sigungu) newIsFocused.sigungu = true;
if (accommodationData.postCode) newIsFocused.postCode = true;
if (accommodationData.fullAddress) newIsFocused.fullAddress = true;
setIsFocused(newIsFocused);
};
checkInitialValue();
}, []);
return (
<StyledContainer>
<StyledInputContainer>
<StyledRowContainer>
<StyledLabel htmlFor="sido" focused={isFocused.sido}>
도/특별・광역시
</StyledLabel>
<StyledInput
id="sido"
defaultValue={accommodationData.sido}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleOnChangeAddress}
/>
</StyledRowContainer>
<StyledRowContainer>
<StyledLabel htmlFor="sigungu" focused={isFocused.sigungu}>
시/군/구
</StyledLabel>
<StyledInput
id="sigungu"
defaultValue={accommodationData.sigungu}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleOnChangeAddress}
/>
</StyledRowContainer>
<StyledRowContainer>
<StyledLabel htmlFor="fullAddress" focused={isFocused.fullAddress}>
전체주소
</StyledLabel>
<StyledInput
id="fullAddress"
defaultValue={accommodationData.fullAddress}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleOnChangeAddress}
/>
</StyledRowContainer>
<StyledRowContainer>
<StyledLabel htmlFor="postCode" focused={isFocused.postCode}>
우편번호
</StyledLabel>
<StyledInput
id="postCode"
defaultValue={accommodationData.postCode}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onChange={handleOnChangeAddress}
/>
</StyledRowContainer>
</StyledInputContainer>
</StyledContainer>
);
}
getLatLngFromAddress.ts
import axios from 'axios';
interface Location {
lat: number;
lng: number;
}
export const getLatLngFromAddress = async (address: string): Promise<Location | null> => {
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${
process.env.REACT_APP_GOOGLE_API_KEY
}`;
const response = await axios.get(url);
const data = response.data;
const location = data.results[0]?.geometry.location;
const latitude = location?.lat;
const longitude = location?.lng;
return { lat: latitude, lng: longitude };
} catch (error) {
console.log(error);
throw error;
}
};
getLatLngFromAddress.ts
export default function RoomInfoItem({ Ref, label, children, editContents, houseId }: RoomInfoItemProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { sendRequest, responseData } = useAuthorizedRequest<any>({});
const { sendRequest: editedSendRequest, responseData: editedResponseData } = useAuthorizedRequest<any>({});
const [originalRoomData, setOriginalRoomData] = useRecoilState(originalRoomDataState);
const [roomData, setRoomData] = useRecoilState(roomDataState);
const formData = useRecoilValue(formDataState);
const [currentRoomDataIndex, setCurrentRoomDataIndex] = useRecoilState(currentRoomDataIndexState);
const [patchRoomData, setPatchRoomData] = useRecoilState(patchRoomReqState);
const openCloseEditComponent = () => {
if (isOpen) {
setRoomData(originalRoomData);
setCurrentRoomDataIndex(0);
setIsOpen(false);
} else {
setCurrentRoomDataIndex(0);
setIsOpen(true);
}
};
const prevMove = () => {
if (currentRoomDataIndex === 0) return;
setCurrentRoomDataIndex(preState => preState - 1);
};
const nextMove = () => {
if (currentRoomDataIndex === roomData.length - 1) return;
setCurrentRoomDataIndex(preState => preState + 1);
};
const dataPatch = async () => {
const newFormData = new FormData();
formData.forEach((value, key) => {
newFormData.append(key, value);
});
const { name, price, minPeople, maxPeople, bedCount, bedroomCount, bathroomCount, totalCount, checkIn, checkOut } =
roomData[currentRoomDataIndex];
const newPatchRoomReq = {
...patchRoomData,
name,
price,
minPeople,
maxPeople,
bedCount,
bedroomCount,
bathroomCount,
totalCount,
checkIn,
checkOut,
};
newFormData.delete('patchRoomReq');
newFormData.append('patchRoomReq', new Blob([JSON.stringify(newPatchRoomReq)], { type: 'application/json' }));
setPatchRoomData(newPatchRoomReq);
try {
await sendRequest({
url: `/user/rooms/${roomData[currentRoomDataIndex].roomId}`,
method: 'PATCH',
body: newFormData,
});
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (!responseData) return;
const fetchData = async () => {
try {
await editedSendRequest({ url: `/api/houses/${houseId}`, method: 'POST' });
} catch (err) {
console.log(err);
}
};
if (responseData.isSuccess) {
fetchData();
setOriginalRoomData(roomData);
const newPatchRoomReq = { ...patchRoomData };
newPatchRoomReq.patchImageReqs = [];
setPatchRoomData(newPatchRoomReq);
alert('객실 수정이 완료되었습니다.');
setIsOpen(preState => !preState);
} else {
alert('객실 수정을 하지못했습니다.');
}
}, [responseData]);
useEffect(() => {
if (!editedResponseData) return;
if (editedResponseData.isSuccess) {
setRoomData(editedResponseData.result.rooms);
setOriginalRoomData(editedResponseData.result.rooms);
}
}, [editedResponseData]);
return (
<StyledItemContainer ref={Ref}>
<StyledItemHeader>
<StyledLabel>{label}</StyledLabel>
<StyledEdit onClick={openCloseEditComponent}>수정</StyledEdit>
</StyledItemHeader>
{isOpen ? (
<StyledEditContainer>
<StyledEditHeader>
<div />
<StyledCloseIcon size={40} onClick={openCloseEditComponent} />
</StyledEditHeader>
<>{editContents}</>
<StyledEditFooter>
<StyledCancelButton onClick={openCloseEditComponent}>취소</StyledCancelButton>
<StyledPageNationDiv>
<StyledMoveButton onClick={prevMove}>
<GrPrevious size={20} />
</StyledMoveButton>
{`${currentRoomDataIndex + 1} / ${roomData.length}`}
<StyledMoveButton onClick={nextMove}>
<GrNext size={20} />
</StyledMoveButton>
</StyledPageNationDiv>
<StyledEditButton onClick={dataPatch}>저장하기</StyledEditButton>
</StyledEditFooter>
</StyledEditContainer>
) : (
<>{children}</>
)}
</StyledItemContainer>
);
}