Skip to content

Host_Manage

SeongWoo Shin edited this page Aug 7, 2023 · 10 revisions

📌 주요 기능

✔ 숙소 삭제

  • REST API 통신(delete)으로 숙소 데이터 삭제 요청
  • useMediaQuery 커스텀 훅으로 브라우저 너비에 따른 스타일 변경

Code

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 객체 메서드 활용)
    • 객실 - 사진 업로드 및 삭제, 페이지네이션

Code

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>
  );
}
Clone this wiki locally