티스토리 뷰
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로 변화된 값을 감지해야하기에!!
이부분이 핵심이다!
'프로젝트' 카테고리의 다른 글
[사이드프로젝트] SR (2) | 2022.04.20 |
---|---|
[프로젝트] SGV 기본 공부해놓기 (0) | 2022.03.03 |
[프로젝트] 비동기에관한 깨닳음 (0) | 2022.02.21 |
[프로젝트][에러] 리코일 저장값의 초기화 (0) | 2022.02.21 |
[프로젝트][에러] 동기, 비동기 (0) | 2022.02.20 |