티스토리 뷰

import styled from "styled-components";
import { useEffect, useRef, useState } from "react";
import iconblack from "../../img/iconblack.png";
import loading from "../../img/loading.gif";
import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil";
import {
  searchLocation,
  mapResultsStorage,
  currentLocationStorage,
  currentLatitude,
  currentLongtitude,
  currentaddress,
  modifyLatitude,
  modifyLongtitude,
} from "../../state/state";
import { KakaoMap } from "../../state/typeDefs";

declare global {
  interface Window {
    kakao: any;
  }
}

const ExchangeLocation = styled.div`
  margin-top: 10px;
  width: 95%;
  height: 400px;
`;

const Conatiner = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 95%;
  height: 400px;
  color: grey;
`;
const Loading = styled.img`
  margin-top: 10px;
`;

const Map = () => {
  const place = useRef(null);
  const [mapLoaded, setMapLoaded] = useState(false);
  const [map, setMap] = useState<KakaoMap | null>(null);
  const [marker, setMarker] = useState<any>(null);
  const [infowindow, setInfoWindow] = useState<any>(new window.kakao.maps.InfoWindow({ zindex: 1 }));
  const [geocoder, setGeocoder] = useState<any>(new window.kakao.maps.services.Geocoder());
  const [currentLocation, setCurrentLocation] = useRecoilState(currentLocationStorage);
  const latitude = useSetRecoilState(currentLatitude);
  const longtitude = useSetRecoilState(currentLongtitude);
  const storeaddress = useSetRecoilState(currentaddress);
  const searchContent = useRecoilValue(searchLocation);
  const setMapSearchResults = useSetRecoilState(mapResultsStorage);
  const modifyLat = useRecoilValue(modifyLatitude);
  const modifyLon = useRecoilValue(modifyLongtitude);
  const imageSrc = iconblack;
  const imageSize = new window.kakao.maps.Size(64, 69);
  const imageOption = { offset: new window.kakao.maps.Point(35, 69) };
  const makerImage = new window.kakao.maps.MarkerImage(imageSrc, imageSize, imageOption);

  function searchDetailAddrFromCoords(coords: any, callback: any) {
    geocoder.coord2Address(coords.getLng(), coords.getLat(), callback);
  }

  function displayMarker(locPosition: any, map: KakaoMap, marker: any) {
    searchDetailAddrFromCoords(locPosition, function (result: any, status: any) {
      storeaddress(
        result[0]?.address.region_1depth_name +
          " " +
          result[0]?.address.region_2depth_name +
          " " +
          result[0]?.address.region_3depth_name
      );

      let detailAddr = !!result[0]?.road_address
        ? "<div>도로명주소 : " + result[0]?.road_address.address_name + "</div>"
        : "";
      detailAddr += "<div>지번 주소 : " + result[0]?.address.address_name + "</div>";

      let content = '<div class="bAddr" style="width:250px; padding:5px">' + detailAddr + "</div>";

      marker.setPosition(locPosition);
      marker.setMap(map);

      infowindow.setContent(content);
      infowindow.open(map, marker);
    });

    window.kakao.maps.event.addListener(map, "click", function (mouseEvent: any) {
      // 클릭한 위도, 경도 정보를 가져옵니다
      searchDetailAddrFromCoords(mouseEvent.latLng, function (result: any, status: any) {
        storeaddress(
          result[0]?.address.region_1depth_name +
            " " +
            result[0]?.address.region_2depth_name +
            " " +
            result[0]?.address.region_3depth_name
        );
        let detailAddr = !!result[0].road_address
          ? "<div>도로명주소 : " + result[0]?.road_address.address_name + "</div>"
          : "";
        detailAddr += "<div>지번 주소 : " + result[0]?.address.address_name + "</div>";

        let content = '<div class="bAddr" style="width:250px; padding:5px">' + detailAddr + "</div>";

        map?.setCenter(mouseEvent.latLng);
        marker.setPosition(mouseEvent.latLng);
        marker.setMap(map);

        latitude(mouseEvent.latLng?.getLat());
        longtitude(mouseEvent.latLng?.getLng());

        infowindow.setContent(content);
        infowindow.open(map, marker);
      });
    });
  }

  useEffect(() => {
    const container = place.current;

    navigator.geolocation.getCurrentPosition((position) => {
      let lat = localStorage.getItem("whichmap") === "등록" ? position.coords.latitude : modifyLat;
      let lon = localStorage.getItem("whichmap") === "등록" ? position.coords.longitude : modifyLon;

      let locPosition = new window.kakao.maps.LatLng(lat, lon);
      let kakaoMap = new window.kakao.maps.Map(container, {
        center: locPosition,
      });

      window.kakao.maps.event.addListener(kakaoMap, "tilesloaded", () => {
        setMapLoaded(true);
      });

      kakaoMap.setCenter(locPosition);
      let newMarker = new window.kakao.maps.Marker({
        map: map,
        position: locPosition,
        image: makerImage,
      });
      displayMarker(locPosition, kakaoMap, newMarker);
      setMarker(newMarker);
      setMap(kakaoMap);
      latitude(lat);
      longtitude(lon);
    });

    return () => {
      setMap(null);
      setMarker(null);
    };
  }, []);

  useEffect(() => {
    latitude(modifyLat);
    longtitude(modifyLon);
    setCurrentLocation({ x: modifyLon, y: modifyLat });
  }, [modifyLat, modifyLon]);

  useEffect(() => {
    const places = new window.kakao.maps.services.Places();
    places.keywordSearch(searchContent, (result: any, status: any) => {
      if (status === window.kakao.maps.services.Status.OK) {
        setMapSearchResults(result);
      } else if (status === window.kakao.maps.services.Status.ZERO_RESULT) {
        alert("검색 결과가 없습니다.");
      } else {
      }
    });
  }, [searchContent]);

  useEffect(() => {
    let coords: any = currentLocation;
    const moveLatLng = new window.kakao.maps.LatLng(coords.y, coords.x);
    searchDetailAddrFromCoords(moveLatLng, function (result: any, status: any) {
      // console.log("여기로 변경", coords);
      latitude(coords.y);
      longtitude(coords.x);
      if (result[0]) {
        storeaddress(
          result[0].address?.region_1depth_name +
            " " +
            result[0].address?.region_2depth_name +
            " " +
            result[0].address?.region_3depth_name
        );

        let detailAddr = !!result[0]?.road_address
          ? "<div>도로명주소 : " + result[0]?.road_address?.address_name + "</div>"
          : "";
        detailAddr += "<div>지번 주소 : " + result[0]?.address?.address_name + "</div>";

        let content = '<div class="bAddr" style="width:250px; padding:5px">' + detailAddr + "</div>";
        marker?.setPosition(moveLatLng);
        marker?.setMap(map);
        map?.panTo(moveLatLng);

        infowindow.setContent(content);
        infowindow.open(map, marker);
      }
    });
  }, [map, currentLocation]);

  return (
    <ExchangeLocation ref={place}>
      {mapLoaded ? null : (
        <Conatiner>
          now loading...
          <Loading src={loading} />
        </Conatiner>
      )}
    </ExchangeLocation>
  );
};

export default Map;

 

 

 

useRecoil의 남용결과 useEffect를 하면서 currentlocation 값이 계속 변화하고 이곳저곳에서 충돌이나면서 디버깅도 힘들어지는 결과가 발생했다.

=> 상위 컴포넌트에서 정확하게 받을수있는 정보들은 useState로 내려줘야한다. userecoil은 초기에 {}로 가져오고 그다음에 채워지기에 다양한 에러변수?가 생긴다.  

 

 

Map / Modify / Upload 수정후 파일

 

//upload.tsx

import styled from "styled-components";
import { useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { useSetRecoilState, useRecoilValue, useRecoilState } from "recoil";
import { Link, useNavigate } from "react-router-dom";
import {
  currentaddress,
  currentLatitude,
  currentLocationStorage,
  currentLongtitude,
  loginState,
  mapResultsStorage,
  searchLocation,
  userState,
} from "../../state/state";
import Swal from "sweetalert2";
import Map from "./Map";
import { useEffect } from "react";
import { postContent } from "../../api";
import { useCallback } from "react";
import loading from "../../img/loading.gif";

declare global {
  interface Window {
    kakao: any;
  }
}

const DisplayRow = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
`;

const DisplayColumn = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto;
`;

const Wrap = styled.div`
  display: flex;
  justify-content: space-between;
  width: 95%;
`;

const Container = styled(DisplayColumn)`
  justify-content: center;
  width: 100%;
  padding-top: 66px;
  max-width: 1400px;
`;

const Form = styled.form`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto;
  width: 80%;
  height: 100%;
`;

const TitleBox = styled.div`
  width: 100%;
  border-bottom: 2px solid rgba(0, 0, 0, 0.5);
  padding-bottom: 10px;
`;

const Title = styled.div`
  color: black;
  font-weight: bold;
  font-size: 23px;
  padding-left: 5px;
`;

const UploadInform = styled.div`
  display: flex;
  width: 100%;
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
  padding: 20px 0px;
`;

const InformBox = styled.div`
  display: flex;
  width: 20%;
`;

const InformTitle = styled.div`
  min-width: 100px;
  font-size: 18px;
  color: #363636f0;
  padding: 0px 0px 0px 10px;
`;

const Uploads = styled.div`
  display: flex;
  align-items: center;
  width: 80%;
`;

const InputBox = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
`;

const Label = styled.label`
  display: flex;
  justify-content: center;
  flex-direction: column;
  align-items: center;
  position: relative;
  z-index: 1;
  width: 150px;
  height: 150px;
  border: 1px solid #dcdbe3;
  background-color: #fafafd;
  i {
    font-size: 30px;
    color: #dcdbe3;
  }
`;

const LabelWrap = styled.div`
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
`;

const ImgFile = styled.input`
  text-decoration: none;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  position: absolute;
  font-size: 0;
  opacity: 0;
`;

interface ErrorProps {
  error: string | undefined;
}

const Input = styled.input<ErrorProps>`
  text-decoration: none;
  border: ${(props) => (props.error ? "2px solid red" : "1px solid rgba(0,0,0,0.2)")};
  width: 100%;
  padding: 8px;
  &:focus {
    outline-color: ${(props) => (props.error ? "red" : "green")};
  }
`;

const Errorbox = styled.div`
  color: red;
  font-weight: bold;
  font-size: 12px;
  padding-left: 5px;
`;

const TitleLength = styled(DisplayRow)`
  width: 8%;
  text-align: right;
  margin: 0px 10px 0px 20px;
`;

const Textarea = styled.textarea<ErrorProps>`
  height: 200px;
  width: 95%;
  border: ${(props) => (props.error ? "2px solid red" : "1px solid rgba(0,0,0,0.2)")};
  &:focus {
    outline-color: ${(props) => (props.error ? "red" : "green")};
  }
`;

const ButtonBox = styled(DisplayRow)`
  width: 100%;
`;

const BookImg = styled.img`
  width: 100%;
  max-height: 100%;
  object-fit: contain;
`;

const ImgTitle = styled.div`
  color: #b2b0b0;
`;

const Button = styled.button`
  min-width: 100px;
  width: 20%;
  height: 50px;
  cursor: pointer;
  font-size: 20px;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  margin: 30px 30px;
`;

const RegisterButton = styled(Button)`
  background-color: #2f6218;
  &:hover {
    background-color: rgba(47, 98, 24, 0.8);
  }
`;
const CancelButton = styled(Link)`
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-width: 100px;
  width: 20%;
  height: 50px;
  cursor: pointer;
  font-size: 20px;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  margin: 30px 30px;
  background-color: #b2b0b0;
  &:hover {
    background-color: rgba(178, 176, 176, 0.8);
  }
`;

const Count = styled.div`
  margin-top: 10px;
`;

const CheckBox = styled.input`
  margin-right: 10px;
`;

const Checklabel = styled.label`
  margin-right: 10px;
  color: grey;
`;
const LocationWrap = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  position: reltative;
`;

const SearchBar = styled.input`
  text-decoration: none;
  border: 1px solid rgba(0, 0, 0, 0.2);
  width: 60%;
  padding: 8px;
  &:focus {
    outline-color: green;
  }
`;

const SearchButton = styled.button`
  cursor: pointer;
  background-color: #2f6218;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  &:hover {
    background-color: rgba(47, 98, 24, 0.8);
  }
  font-size: 15px;
  transition: 0.3s;
  padding: 0 15px;
  i {
    color: white;
    font-size: 18px;
  }
`;

const ImgMapList = styled.div`
  display: flex;
  border: 1px solid #dcdbe3;
  width: 150px;
  height: 150px;
  padding: 10px;
  justify-content: center;
  align-items: center;
  margin-left: 20px;
`;

const SearchContainer = styled.div`
  width: 100%;
  position: relative;
`;

const CheckBoxWrap = styled.div``;

const SearchBox = styled.div`
  display: flex;
  width: 100%;
`;
const SearchResultBox = styled.div`
  position: absolute;
  width: 60%;
  z-index: 20;
  border: 1px solid rgba(0, 0, 0, 0.2);
`;

const SearchResult = styled.div`
  background-color: white;
  padding: 2px;
  cursor: pointer;
`;

const MapLoadingContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 95%;
  height: 400px;
  color: grey;
`;
const LoadingConatiner = styled.img`
  margin-top: 10px;
`;

//file받아오고 file수만큼 이미지를 만들어준다.
type FormData = {
  img: FileList;
  title: string;
  content: string;
  quality: string;
  location: string;
  latitude: number;
  longtitude: number;
};

const Upload = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [imageStore, setImageStore] = useState<any[]>([]);
  const [imageUrls, setImageUrls] = useState<string[]>([]);
  const setLocation = useSetRecoilState(searchLocation);
  const [currentLocation, setCurrentLocation] = useRecoilState(currentLocationStorage);
  const mapSearchResults = useRecoilValue(mapResultsStorage);
  const token = useRecoilValue(loginState);
  const userInfo = useRecoilValue(userState);
  const latitude = useRecoilValue(currentLatitude);
  const longtitude = useRecoilValue(currentLongtitude);
  const address = useRecoilValue(currentaddress);
  const navigate = useNavigate();

  const side = useRef<HTMLDivElement>(null);
  const {
    register,
    getValues,
    watch,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({ mode: "onChange" });

  const { title } = watch();
  const { content } = watch();

  const postData = async () => {
    return new Promise(async (res, rej) => {
      const { title, content, quality } = getValues();
      const formData = new FormData();
      formData.append("title", title);
      formData.append("content", content);
      if (imageStore) {
        for (let i = 0; i < imageStore.length; i++) {
          formData.append("file", imageStore[i]);
        }
      }
      formData.append("quality", quality);
      formData.append("lat", String(currentLocation.y));
      formData.append("lon", String(currentLocation.x));
      formData.append("address", address);
      let status = await postContent(formData, token || "token");
      if (Number(status) < 300) {
        res(true);
      } else {
        rej(false);
      }
    });
  };

  const onSubmit = async () => {
    const { quality } = getValues();
    if (quality === null) {
      return Swal.fire({
        text: "상품 상태를 선택해주세요",
        confirmButtonText: "확f인",
        confirmButtonColor: "#2f6218",
        icon: "warning",
      });
    }
    try {
      let successful = await postData();
      if (successful) {
        navigate("/search", { replace: true });
      }
    } catch (e) {
      throw e;
    }
  };

  const searchPlace = async () => {
    setIsOpen(!isOpen);
    const { location } = getValues();
    setLocation(location);
  };

  const handleClickOutside = useCallback(
    (event: CustomEvent<MouseEvent>) => {
      if (isOpen && !side?.current?.contains(event.target as Node)) {
        setIsOpen(!isOpen);
      }
    },
    [setIsOpen, isOpen]
  );

  const convertFileToURL = (file: File) => {
    return new Promise<string>((res, rej) => {
      const fileReader = new FileReader();
      fileReader.onerror = rej;
      fileReader.onload = () => {
        res(String(fileReader.result));
      };
      fileReader.readAsDataURL(file);
    });
  };

  const convertManyFilesToURL = async (files: FileList) => {
    let promises = [];
    for (let i = 0; i < files.length; i++) {
      promises.push(convertFileToURL(files[i]));
    }
    try {
      return await Promise.all<string>(promises);
    } catch (e) {
      throw e;
    }
  };

  useEffect(() => {
    window.addEventListener("click", handleClickOutside as EventListener);
    return () => {
      window.removeEventListener("click", handleClickOutside as EventListener);
    };
  }, [handleClickOutside]);

  useEffect(() => {
    (async () => {
      let permission = await navigator.permissions.query({ name: "geolocation" });
      if (permission.state === "granted") {
        navigator.geolocation.getCurrentPosition((position) => {
          setCurrentLocation({ x: position.coords.longitude, y: position.coords.latitude });
        });
      } else {
        setCurrentLocation({ x: userInfo.locations.lat, y: userInfo.locations.lon });
      }
    })();

    return () => {
      setCurrentLocation({ addressName: "", x: 0, y: 0 });
    };
  }, []);

  return (
    <Container>
      <Form onSubmit={handleSubmit(onSubmit)}>
        <TitleBox>
          <Title>도서 등록</Title>
        </TitleBox>
        <UploadInform>
          <InformBox>
            <InformTitle>제목</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <Input
                type="text"
                placeholder="도서명을 입력해주세요."
                error={errors.title?.message}
                {...register("title", {
                  required: "제목을 입력해주세요",
                  maxLength: { value: 20, message: "최대 20자 이하로 입력해주세요" },
                })}
              />
              <Errorbox>{errors.title?.message}</Errorbox>
            </InputBox>
            <TitleLength>{title === undefined ? "0/20" : `${title.length}/20`}</TitleLength>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>상태</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <CheckBoxWrap>
                <CheckBox type="radio" id="새상품같음" value="새상품같음" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="새상품같음">새상품같음</Checklabel>
                <CheckBox type="radio" id="약간헌책" value="약간헌책" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="약간헌책w">약간헌책</Checklabel>
                <CheckBox type="radio" id="많이헌책" value="많이헌책" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="많이헌책">많이헌책</Checklabel>
              </CheckBoxWrap>
              <Errorbox>{errors.quality?.message}</Errorbox>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>사진</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <LabelWrap>
                <Label htmlFor="input_file">
                  <i className="fas fa-camera"></i>
                  <ImgTitle>이미지 업로드</ImgTitle>
                  <ImgFile
                    id="input_file"
                    type="file"
                    accept="image/*"
                    multiple
                    {...register("img", {
                      required: "이미지를 업로드해주세요",
                      onChange: async (event) => {
                        let files = event.target.files;
                        if (files && files.length) {
                          if (imageUrls.length > 2) {
                            return Swal.fire({
                              text: "사진첨부는 최대 3장까지 가능합니다",
                              confirmButtonText: "확인",
                              confirmButtonColor: "#2f6218",
                              icon: "warning",
                            });
                          }
                          let urls = await convertManyFilesToURL(files);
                          setImageUrls((prev) => {
                            return [...prev, ...urls];
                          });
                          setImageStore([...imageStore, ...files]);
                        }
                      },
                    })}
                  />
                </Label>
                {imageUrls.map((url, key) => {
                  return (
                    <ImgMapList key={key}>
                      <BookImg src={url}></BookImg>
                    </ImgMapList>
                  );
                })}
              </LabelWrap>
              <Errorbox>{errors.img?.message}</Errorbox>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>설명</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <Textarea
                placeholder="상품 설명을 입력해주세요.(10글자이상)"
                error={errors.content?.message}
                {...register("content", {
                  required: "상품 설명을 입력해주세요.",
                  minLength: { value: 10, message: "상품 설명을 10자 이상 입력해주세요" },
                  maxLength: { value: 500, message: "상품 설명을 500자 이하 입력해주세요" },
                })}
              />
              <Wrap>
                <Errorbox>{errors.content?.message}</Errorbox>
                <Count>{content === undefined ? "0/200" : `${content.length}/500`}</Count>
              </Wrap>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>위치</InformTitle>
          </InformBox>
          <Uploads>
            <LocationWrap>
              <SearchContainer>
                <SearchBox>
                  <SearchBar type="text" placeholder="건물,지역 검색" {...register("location")}></SearchBar>
                  <SearchButton type="button" onClick={searchPlace}>
                    <i className="fas fa-search"></i>
                  </SearchButton>
                </SearchBox>
                {isOpen ? (
                  <SearchResultBox ref={side}>
                    {mapSearchResults.map((searchResult: { address_name: string }, key) => {
                      return (
                        <SearchResult
                          key={key}
                          onClick={() => {
                            setIsOpen(!isOpen);
                            setCurrentLocation(searchResult);
                          }}
                        >
                          {searchResult?.address_name}
                        </SearchResult>
                      );
                    })}
                  </SearchResultBox>
                ) : null}
              </SearchContainer>
              {currentLocation.x && currentLocation.y ? (
                <Map mapLat={currentLocation?.y} mapLong={currentLocation?.x} />
              ) : (
                <MapLoadingContainer>
                  now loading...
                  <LoadingConatiner src={loading} />
                </MapLoadingContainer>
              )}
            </LocationWrap>
          </Uploads>
        </UploadInform>
        <ButtonBox>
          <CancelButton to="/search">취소</CancelButton>
          <RegisterButton type="submit">등록</RegisterButton>
        </ButtonBox>
      </Form>
    </Container>
  );
};

export default Upload;

modify.tsx

import styled from "styled-components";
import { useState, useRef, useEffect, useCallback } from "react";
import { useForm } from "react-hook-form";
import { useSetRecoilState, useRecoilValue, useRecoilState } from "recoil";
import { Link, useNavigate } from "react-router-dom";
import {
  currentaddress,
  currentLocationStorage,
  loginState,
  mapResultsStorage,
  searchLocation,
  storeContentId,
} from "../../state/state";
import Swal from "sweetalert2";
import Map from "../Book/Map";
import { getSingleBookInfo, patchContent } from "../../api";

declare global {
  interface Window {
    kakao: any;
  }
}

const DisplayRow = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
`;

const DisplayColumn = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto;
`;

const Wrap = styled.div`
  display: flex;
  justify-content: space-between;
  width: 95%;
`;

const Container = styled(DisplayColumn)`
  justify-content: center;
  width: 100%;
  padding-top: 66px;
  max-width: 1400px;
`;

const Form = styled.form`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto;
  width: 80%;
  height: 100%;
`;

const TitleBox = styled.div`
  width: 100%;
  border-bottom: 2px solid rgba(0, 0, 0, 0.5);
  padding-bottom: 10px;
`;

const Title = styled.div`
  color: black;
  font-weight: bold;
  font-size: 23px;
  padding-left: 5px;
`;

const UploadInform = styled.div`
  display: flex;
  width: 100%;
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
  padding: 20px 0px;
`;

const InformBox = styled.div`
  display: flex;
  width: 20%;
`;

const InformTitle = styled.div`
  min-width: 100px;
  font-size: 18px;
  color: #363636f0;
  padding: 0px 0px 0px 10px;
`;

const Uploads = styled.div`
  display: flex;
  align-items: center;
  width: 80%;
`;

const InputBox = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
`;

const Label = styled.label`
  display: flex;
  justify-content: center;
  flex-direction: column;
  align-items: center;
  position: relative;
  z-index: 1;

  width: 150px;
  height: 150px;
  border: 1px solid #dcdbe3;
  background-color: #fafafd;
  i {
    font-size: 30px;
    color: #dcdbe3;
  }
`;

const ImgFile = styled.input`
  text-decoration: none;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  position: absolute;
  font-size: 0;
  opacity: 0;
`;

const ImgMapList = styled.div`
  display: flex;
  border: 1px solid #dcdbe3;
  width: 150px;
  height: 150px;
  padding: 10px;
  justify-content: center;
  align-items: center;
  margin-left: 20px;
`;

interface ErrorProps {
  error: string | undefined;
}

const Input = styled.input<ErrorProps>`
  text-decoration: none;
  border: ${(props) => (props.error ? "2px solid red" : "1px solid rgba(0,0,0,0.2)")};
  width: 100%;
  padding: 8px;
  &:focus {
    outline-color: ${(props) => (props.error ? "red" : "green")};
  }
`;

const Errorbox = styled.div`
  color: red;
  font-weight: bold;
  font-size: 12px;
  padding-left: 5px;
`;

const TitleLength = styled(DisplayRow)`
  width: 8%;
  text-align: right;
  margin: 0px 10px 0px 20px;
`;

const Textarea = styled.textarea<ErrorProps>`
  height: 200px;
  width: 95%;
  border: ${(props) => (props.error ? "2px solid red" : "1px solid rgba(0,0,0,0.2)")};
  &:focus {
    outline-color: ${(props) => (props.error ? "red" : "green")};
  }
`;

const ButtonBox = styled(DisplayRow)`
  width: 100%;
`;

const BookImg = styled.img`
  width: 100%;
  max-height: 100%;
  object-fit: contain;
`;

const ImgTitle = styled.div`
  color: #b2b0b0;
`;

const Button = styled.button`
  min-width: 100px;
  width: 20%;
  height: 50px;
  cursor: pointer;
  font-size: 20px;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  margin: 30px 30px;
`;

const RegisterButton = styled(Button)`
  background-color: #2f6218;
  &:hover {
    background-color: rgba(47, 98, 24, 0.8);
  }
`;
const CancelButton = styled(Link)`
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-width: 100px;
  width: 20%;
  height: 50px;
  cursor: pointer;
  font-size: 20px;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  margin: 30px 30px;
  background-color: #b2b0b0;
  &:hover {
    background-color: rgba(178, 176, 176, 0.8);
  }
`;

const Count = styled.div`
  margin-top: 10px;
`;

const CheckBox = styled.input`
  margin-right: 10px;
`;

const Checklabel = styled.label`
  margin-right: 10px;
  color: grey;
`;
const LocationWrap = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  position: reltative;
`;

const SearchBar = styled.input`
  text-decoration: none;
  border: 1px solid rgba(0, 0, 0, 0.2);
  width: 60%;
  padding: 8px;
  &:focus {
    outline-color: green;
  }
`;

const SearchButton = styled.button`
  cursor: pointer;
  background-color: #2f6218;
  border: 0;
  outline: 0;
  color: rgb(242, 242, 242, 0.9);
  font-weight: 500;
  &:hover {
    background-color: rgba(47, 98, 24, 0.8);
  }
  font-size: 15px;
  transition: 0.3s;
  padding: 0 15px;
  i {
    color: white;
    font-size: 18px;
  }
`;

const SearchContainer = styled.div`
  width: 100%;
  position: relative;
`;

const CheckBoxWrap = styled.div``;

const SearchBox = styled.div`
  display: flex;
  width: 100%;
`;
const SearchResultBox = styled.div`
  position: absolute;
  width: 60%;
  z-index: 20;
  border: 1px solid rgba(0, 0, 0, 0.2);
`;

const SearchResult = styled.div`
  background-color: white;
  padding: 2px;
  cursor: pointer;
`;
//file받아오고 file수만큼 이미지를 만들어준다.
type FormData = {
  img: FileList;
  title: string;
  content: string;
  quality: string;
  location: string;
  latitude: number;
  longtitude: number;
};

const Modify = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [imageStore, setImageStore] = useState<any[]>([]);
  const [imageUrls, setImageUrls] = useState<string[]>([]);
  const [patchImageUrls, setPatchImageUrls] = useState<string[]>([]);
  const setLocation = useSetRecoilState(searchLocation);
  const [currentLocation, setCurrentLocation] = useRecoilState(currentLocationStorage);

  const [modifyLatitu, setModifyLatitu] = useState();
  const [modifyLongtitu, setModifyLongtitu] = useState();

  const mapSearchResults = useRecoilValue(mapResultsStorage);
  const token = useRecoilValue(loginState);
  const address = useRecoilValue(currentaddress);
  const navigate = useNavigate();
  const id = useRecoilValue(storeContentId);
  const [modifyquality, setModifyQuality] = useState<string>("");
  const pageChange = Number(localStorage.getItem("modify_id"));

  const side = useRef<HTMLDivElement>(null);
  const {
    register,
    getValues,
    watch,
    handleSubmit,
    setValue,
    formState: { errors },
  } = useForm<FormData>({ mode: "onChange" });

  const { title } = watch();
  const { content } = watch();

  const convertFileToURL = (file: File) => {
    return new Promise<string>((res, rej) => {
      const fileReader = new FileReader();
      fileReader.onerror = rej;
      fileReader.onload = () => {
        res(String(fileReader.result));
      };
      fileReader.readAsDataURL(file);
    });
  };

  const convertManyFilesToURL = async (files: FileList) => {
    let promises = [];
    for (let i = 0; i < files?.length; i++) {
      promises.push(convertFileToURL(files[i]));
    }
    try {
      return await Promise.all<string>(promises);
    } catch (e) {
      throw e;
    }
  };

  const getSingleData = async (id: number) => {
    const { productInfo } = await getSingleBookInfo(id, token);
    setValue("title", productInfo.title);
    setValue("content", productInfo.content);
    const radiobuttonValue = document.getElementById(productInfo.quality) as HTMLInputElement;
    radiobuttonValue.checked = true;
    setModifyQuality(productInfo.quality);
    let modifyImg = productInfo.images;
    let imgData: any[] = [];
    for (let i = 0; i < modifyImg.length; i++) {
      imgData.push(modifyImg[i].url);
    }

    setPatchImageUrls((prev) => {
      return [...prev, ...imgData];
    });
    setImageUrls((prev) => {
      return [...prev, ...imgData];
    });
    setModifyLatitu(productInfo.locations.lat);
    setModifyLongtitu(productInfo.locations.lon);
  };

  const patchData = async () => {
    return new Promise(async (res, rej) => {
      const { title, content, quality } = getValues();

      const formData = new FormData();
      formData.append("title", title);
      formData.append("content", content);
      if (imageStore) {
        for (let i = 0; i < imageStore.length; i++) {
          formData.append("file", imageStore[i]);
        }
      }

      for (let i = 0; i < patchImageUrls.length; i++) {
        formData.append("url", JSON.stringify({ url: patchImageUrls[i], productId: pageChange }));
      }

      if (quality === null) {
        formData.append("quality", modifyquality);
      } else {
        formData.append("quality", quality);
      }
      formData.append("lat", String(currentLocation.y));
      formData.append("lon", String(currentLocation.x));
      formData.append("address", address);

      let status = await patchContent(Number(id), formData, token || "token");
      if (Number(status) < 300) {
        res(true);
      } else {
        rej(false);
      }
    });
  };

  const onSubmit = async () => {
    try {
      let successful = await patchData();
      if (successful) {
        navigate("/search", { replace: true });
      }
    } catch (e) {
      throw e;
    }
  };

  const searchPlace = async () => {
    setIsOpen(!isOpen);
    const { location } = getValues();
    setLocation(location);
  };

  const handleClickOutside = useCallback(
    (event: CustomEvent<MouseEvent>) => {
      if (isOpen && !side?.current?.contains(event.target as Node)) {
        setIsOpen(!isOpen);
      }
    },
    [isOpen]
  );

  useEffect(() => {
    window.addEventListener("click", handleClickOutside as EventListener);
    return () => {
      window.removeEventListener("click", handleClickOutside as EventListener);
    };
  }, [isOpen, handleClickOutside]);

  useEffect(() => {
    let fetchID = id;
    if (fetchID === null) {
      fetchID = Number(localStorage.getItem("modify_id"));
    }
    getSingleData(fetchID);
    return () => {
      localStorage.removeItem("modify_id");
    };
  }, [id]);

  useEffect(() => {
    return () => {
      setCurrentLocation({ addressName: "", x: 0, y: 0 });
    };
  }, []);

  return (
    <Container>
      <Form onSubmit={handleSubmit(onSubmit)}>
        <TitleBox>
          <Title>게시글 수정</Title>
        </TitleBox>
        <UploadInform>
          <InformBox>
            <InformTitle>제목</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <Input
                type="text"
                placeholder="도서명을 입력해주세요."
                error={errors.title?.message}
                {...register("title", {
                  required: "제목을 입력해주세요",
                  maxLength: { value: 20, message: "최대 20자 이하로 입력해주세요" },
                })}
              />
              <Errorbox>{errors.title?.message}</Errorbox>
            </InputBox>
            <TitleLength>{title === undefined ? "0/20" : `${title.length}/20`}</TitleLength>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>상태</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <CheckBoxWrap>
                <CheckBox type="radio" id="새상품같음" value="새상품같음" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="새상품같음">새상품같음</Checklabel>
                <CheckBox type="radio" id="약간헌책" value="약간헌책" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="약간헌책w">약간헌책</Checklabel>
                <CheckBox type="radio" id="많이헌책" value="많이헌책" {...register("quality")}></CheckBox>
                <Checklabel htmlFor="많이헌책">많이헌책</Checklabel>
              </CheckBoxWrap>
              <Errorbox>{errors.quality?.message}</Errorbox>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>사진</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <div
                style={{
                  display: "flex",
                  alignItems: "stretch",
                  justifyContent: "flex-start",
                  width: "100%",
                }}
              >
                <Label htmlFor="input_file">
                  <i className="fas fa-camera"></i>
                  <ImgTitle>이미지 업로드</ImgTitle>
                  <ImgFile
                    id="input_file"
                    type="file"
                    accept="image/*"
                    multiple
                    {...register("img", {
                      onChange: async (event) => {
                        let files = event.target.files;
                        if (files && files.length) {
                          if (imageUrls.length > 2) {
                            return Swal.fire({
                              text: "사진첨부는 최대 3장까지 가능합니다",
                              confirmButtonText: "확인",
                              confirmButtonColor: "#2f6218",
                              icon: "warning",
                            });
                          }
                          let urls = await convertManyFilesToURL(files);
                          setImageUrls((prev) => {
                            return [...prev, ...urls];
                          });
                          setImageStore([...imageStore, ...files]);
                        }
                      },
                    })}
                  />
                </Label>
                {imageUrls.map((url, key) => {
                  return (
                    <ImgMapList key={key}>
                      <BookImg src={url}></BookImg>
                    </ImgMapList>
                  );
                })}
              </div>
              <Errorbox>{errors.img?.message}</Errorbox>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>설명</InformTitle>
          </InformBox>
          <Uploads>
            <InputBox>
              <Textarea
                placeholder="상품 설명을 입력해주세요.(10글자이상)"
                error={errors.content?.message}
                {...register("content", {
                  required: "상품 설명을 입력해주세요.",
                  minLength: { value: 10, message: "상품 설명을 10자 이상 입력해주세요" },
                  maxLength: { value: 500, message: "상품 설명을 500자 이하 입력해주세요" },
                })}
              />
              <Wrap>
                <Errorbox>{errors.content?.message}</Errorbox>
                <Count>{content === undefined ? "0/200" : `${content.length}/500`}</Count>
              </Wrap>
            </InputBox>
          </Uploads>
        </UploadInform>
        <UploadInform>
          <InformBox>
            <InformTitle>위치</InformTitle>
          </InformBox>
          <Uploads>
            <LocationWrap>
              <SearchContainer>
                <SearchBox>
                  <SearchBar type="text" placeholder="건물,지역 검색" {...register("location")}></SearchBar>
                  <SearchButton type="button" onClick={searchPlace}>
                    <i className="fas fa-search"></i>
                  </SearchButton>
                </SearchBox>
                {isOpen ? (
                  <SearchResultBox ref={side}>
                    {mapSearchResults.map((searchResult: any, key) => {
                      return (
                        <SearchResult
                          key={key}
                          onClick={() => {
                            setIsOpen(!isOpen);
                            setCurrentLocation(searchResult);
                            setModifyLatitu(searchResult.y);
                            setModifyLongtitu(searchResult.x);
                          }}
                        >
                          {searchResult?.address_name}
                        </SearchResult>
                      );
                    })}
                  </SearchResultBox>
                ) : null}
              </SearchContainer>
              {modifyLatitu && modifyLongtitu ? <Map mapLat={modifyLatitu} mapLong={modifyLongtitu} /> : null}
            </LocationWrap>
          </Uploads>
        </UploadInform>
        <ButtonBox>
          <CancelButton to={`/search/${pageChange}`}>취소</CancelButton>
          <RegisterButton type="submit">수정</RegisterButton>
        </ButtonBox>
      </Form>
    </Container>
  );
};

export default Modify;

 

 

✔️ 바뀐 중요한부분을 살펴보자

 

upload페이지는

 

1. 검색하면 현재위치가 바뀌고 현재이위치를 담을 state를 만들어준다. 리코일 스테이트로 만들어봤다. (내려줄것이므로 괜찮다)

  const [currentLocation, setCurrentLocation] = useRecoilState(currentLocationStorage);

 

2. 현재 사용자가 위치정보를 허용했으면? => 지금 있는 위치를 나타낼것.  // 아니면? => 사용자가 등록한 위치로 나올것

두가지로 분기를 해야하는데  네비게이터 권한이란게있었다. 

https://developer.mozilla.org/en-US/docs/Web/API/Navigator/permissions

이부분을 약간 수정하여 

setCurrentLocation을 넣어준다.

 

y가 latitude고 x가 longtitude로 되어있기에 맞게끔 설정해준것. 

  useEffect(() => {
    (async () => {
      let permission = await navigator.permissions.query({ name: "geolocation" });
      if (permission.state === "granted") {
        navigator.geolocation.getCurrentPosition((position) => {
          setCurrentLocation({ x: position.coords.longitude, y: position.coords.latitude });
        });
      } else {
        setCurrentLocation({ x: userInfo.locations.lat, y: userInfo.locations.lon });
      }
    })();

    return () => {
      setCurrentLocation({ addressName: "", x: 0, y: 0 });
    };
  }, []);

아래 Userinfo.lat lon부분은 찍어보니 lat lon이 거꾸로들어와서 바꿔줬다. 요것은 다시수정해둘것 체크!

리턴부분은 렌더링이 안될때 초기화르 해주는값이다. 

이렇게 현재 위도경도의 값을 Map에다가 내려줄것이다.

        {currentLocation.x && currentLocation.y ? (
                <Map mapLat={currentLocation?.y} mapLong={currentLocation?.x} />
              ) : (
                <MapLoadingContainer>
                  now loading...
                  <LoadingConatiner src={loading} />
                </MapLoadingContainer>
              )}

 

 

Map부분은 이후에 보자.

 

Modify를 보면

 

Modify에서는 사용자가 이전에 올려놨던 위치정보를 가져와야하는것이 핵심

아래는 그것을 담을 state이다.

  const [modifyLatitu, setModifyLatitu] = useState();
  const [modifyLongtitu, setModifyLongtitu] = useState();

리턴아래부분. 

  {modifyLatitu && modifyLongtitu ? <Map mapLat={modifyLatitu} mapLong={modifyLongtitu} /> : null}

 

Map으로가보자 

 

import styled from "styled-components";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import iconblack from "../../img/iconblack.png";
import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil";
import {
  searchLocation,
  mapResultsStorage,
  currentLatitude,
  currentLongtitude,
  currentaddress,
} from "../../state/state";
import { KakaoMap } from "../../state/typeDefs";

declare global {
  interface Window {
    kakao: any;
  }
}

const ExchangeLocation = styled.div`
  margin-top: 10px;
  width: 95%;
  height: 400px;
`;

const Map = ({ mapLat, mapLong }: { mapLat: any; mapLong: any }) => {
  const place = useRef(null);
  // const [mapLoaded, setMapLoaded] = useState(false);
  const [map, setMap] = useState<KakaoMap | null>(null);
  const [marker, setMarker] = useState<any>(null);
  const [infowindow, setInfoWindow] = useState<any>(
    useCallback(() => new window.kakao.maps.InfoWindow({ zindex: 1 }), [])
  );
  const [geocoder, setGeocoder] = useState<any>(useCallback(() => new window.kakao.maps.services.Geocoder(), []));
  const setLatitude = useSetRecoilState(currentLatitude);
  const setLongitude = useSetRecoilState(currentLongtitude);
  const storeaddress = useSetRecoilState(currentaddress);
  const searchContent = useRecoilValue(searchLocation);
  const setMapSearchResults = useSetRecoilState(mapResultsStorage);
  const imageSrc = iconblack;
  const imageSize = useMemo(() => new window.kakao.maps.Size(64, 69), []);
  const imageOption = useMemo(() => {
    return { offset: new window.kakao.maps.Point(35, 69) };
  }, []);
  const makerImage = useMemo(
    () => new window.kakao.maps.MarkerImage(imageSrc, imageSize, imageOption),
    [imageOption, imageSize, imageSrc]
  );

  const displayMarker = (map: KakaoMap, marker: any, locPosition: any) => {
    geocoder.coord2Address(locPosition.getLng(), locPosition.getLat(), (result: any, status: any) => {
      storeaddress(
        result[0]?.address.region_1depth_name +
          " " +
          result[0]?.address.region_2depth_name +
          " " +
          result[0]?.address.region_3depth_name
      );

      let detailAddr = !!result[0]?.road_address
        ? "<div>도로명주소 : " + result[0]?.road_address.address_name + "</div>"
        : "";
      detailAddr += "<div>지번 주소 : " + result[0]?.address.address_name + "</div>";

      let content = '<div class="bAddr" style="width:250px; padding:5px">' + detailAddr + "</div>";

      marker.setPosition(locPosition);
      marker.setMap(map);

      infowindow.setContent(content);
      infowindow.open(map, marker);
    });
  };

  const addClickListener = (map: KakaoMap, marker: any) => {
    window.kakao.maps.event.removeListener(map, "click");
    window.kakao.maps.event.addListener(map, "click", (mouseEvent: any) => {
      // 클릭한 위도, 경도 정보를 가져옵니다
      let mouseLat = mouseEvent.latLng.getLat();
      let mouseLon = mouseEvent.latLng.getLng();

      geocoder.coord2Address(mouseLon, mouseLat, (result: any, status: any) => {
        if (!result) return;
        storeaddress(
          result[0]?.address.region_1depth_name +
            " " +
            result[0]?.address.region_2depth_name +
            " " +
            result[0]?.address.region_3depth_name
        );
        let detailAddr = !!result[0]?.road_address
          ? "<div>도로명주소 : " + result[0]?.road_address.address_name + "</div>"
          : "";
        detailAddr += "<div>지번 주소 : " + result[0]?.address.address_name + "</div>";

        let content = '<div class="bAddr" style="width:250px; padding:5px">' + detailAddr + "</div>";

        map?.setCenter(mouseEvent.latLng);
        marker.setPosition(mouseEvent.latLng);
        marker.setMap(map);

        setLatitude(mouseEvent.latLng?.getLat());
        setLongitude(mouseEvent.latLng?.getLng());

        infowindow.setContent(content);
        infowindow.open(map, marker);
      });
    });
  };

  useEffect(() => {
    let locPosition = new window.kakao.maps.LatLng(mapLat, mapLong);
    if (map) {
      displayMarker(map, marker, locPosition);
      map.panTo(locPosition);
    }
  }, [mapLat, mapLong]);

  useEffect(() => {
    const container = place.current;
    let lat = mapLat;
    let lon = mapLong;
    let locPosition = new window.kakao.maps.LatLng(lat, lon);
    let kakaoMap;
    if (map === null) {
      kakaoMap = new window.kakao.maps.Map(container, {
        center: locPosition,
      });
    } else {
      kakaoMap = map;
    }

    let newMarker = new window.kakao.maps.Marker({
      map: kakaoMap,
      position: locPosition,
      image: makerImage,
    });

    displayMarker(kakaoMap, newMarker, locPosition);
    addClickListener(kakaoMap, newMarker);
    setMarker(newMarker);
    setMap(kakaoMap);
    setLatitude(lat);
    setLongitude(lon);

    return () => {
      setMap(null);
      setMarker(null);
    };
  }, []);

  useEffect(() => {
    const places = new window.kakao.maps.services.Places();
    places.keywordSearch(searchContent, (result: any, status: any) => {
      if (status === window.kakao.maps.services.Status.OK) {
        setMapSearchResults(result);
      } else if (status === window.kakao.maps.services.Status.ZERO_RESULT) {
        alert("검색 결과가 없습니다.");
      } else {
      }
    });
  }, [setMapSearchResults, searchContent]);

  return <ExchangeLocation ref={place} />;
};

export default Map;

요기서 설명해야할것은 그렇게 많지가않고..

 

  useEffect(() => {
    let locPosition = new window.kakao.maps.LatLng(mapLat, mapLong);
    if (map) {
      displayMarker(map, marker, locPosition);
      map.panTo(locPosition);
    }
  }, [mapLat, mapLong]);

이부분을 써야하는 이유는

검색을 햇을때 current location. x , y 는 계속 변하는데 그것을 map에 표시를 해줄려면 useeffect로 변화된 값을 감지해야하기에!!

이부분이 핵심이다!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함