๋ฐฐํฌ ์ฌ์ดํธ : https://foodzip.netlify.app/
- Email : [email protected]
- Password : 111111!
FOODZIP์ ์์ฌ๋ฅผ ์ฆ๊ธฐ๋ฉฐ ๋ง์๋ ์์๊ณผ ํ๋ฅญํ ์๋น์ ์ฐพ๋ ์ด๋ค์ ์ํ ์๋น ๊ณต์ ์ปค๋ฎค๋ํฐ์ ๋๋ค. ์ด ํ๋ก์ ํธ๋ ์ฌ์ฉ์๋ค์ด ๋ง์๋ ์์์ ์ฐพ๊ณ ๊ณต์ ํ๋ฉฐ ์ฆ๊ฑฐ์ด ์์ฌ ๊ฒฝํ์ ๊ณต์ ํ๋ ๋ฐ ์ด์ ์ ๋ง์ถ๊ณ ์์ต๋๋ค. ํด๋น ์ปค๋ฎค๋ํฐ๋ฅผ ํตํด ์ฌ์ฉ์๋ค์ ์ํ๋ ์๋น์ ์ฝ๊ฒ ์ฐพ๊ณ , ๋ค์ํ ์์์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ ์ ์์ผ๋ฉฐ, ๊ฐ์ ๊ด์ฌ์ฌ๋ฅผ ๊ฐ์ง ์ฌ๋๋ค๊ณผ ์ํตํ ์ ์์ต๋๋ค. ํจ๊ป ๋ง์๋ ์์๊ณผ ์ฆ๊ฑฐ์ด ์์ฌ๋ฅผ ์ฆ๊ธธ ์ ์๋ FOODZIP์ ์ฌ๋ฌ๋ถ์ ์ด๋ํฉ๋๋ค!
์ด์ง์ ์กฐ์ฅ |
๊น์จ์ด ์กฐ์ |
์ค์ ํธ ์กฐ์ |
์ด์์ฃผ ์กฐ์ |
-
ํฌ๋ช ํ ์ ๋ณด ๊ณต์ : ํ์ ์ ์์ด ์๋ก์ ์์ ์ ๋ํ ์ดํด๋ฅผ ๊น๊ฒ ํฉ๋๋ค.
-
์ ์ํ ์๊ฒฌ ์กฐ์จ : ์คํ ์๋์ ์ฑ ์๊ฐ์ด ๋์์ง๋๋ค.
-
์ง์ ์ฅ๋ฒฝ ๋ฎ์ถ๊ธฐ : ํ ๊ฐ์ ์ง์ ์ฅ๋ฒฝ์ ๋ฎ์ถ๊ณ , ํ์ ๋ชจ๋๊ฐ ํ๋ก์ ํธ์ ์ ๊ทน์ ์ผ๋ก ์ฐธ์ฌ ํ๋ ํ๊ฒฝ์ ๋ง๋ค์์ต๋๋ค.
-
๊ฑด๊ฐํ ํ ๋ถ์๊ธฐ : ์ํํ ์์ฌ์ํต์ ํตํด ์ํธ ์ ๋ขฐ๋ฅผ ์์๊ฐ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ชจ๋ ํ์์ด ํ๋ก์ ํธ๋ฅผ ์ ๊ทน์ ์ผ๋ก ์ฐธ์ฌํ๋ ๋ถ์๊ธฐ๋ฅผ ์กฐ์ฑํ์ต๋๋ค.
-
ํ์ ์ ํจ์จ์ฑ ๊ทน๋ํ : ํผ๋๋ฐฑ ๊ณผ์ ์ ํตํด ์์ ๊ณผ์ ์ ๊ฐ์ ํ๊ณ ํ์๋ค๋ผ๋ฆฌ ์๋ก๋ฅผ ํ๊ธฐํ ์ ์์ต๋๋ค.
์ฝ๋ | ์ค๋ช |
---|---|
import useForm |
react-hook-form ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ์ํด useForm ์ ๊ฐ์ ธ์ต๋๋ค. |
useForm |
useForm ํ
์ ํธ์ถํ์ฌ ํ์ํ ๋ฉ์๋์ ์์ฑ์ ์ถ์ถํฉ๋๋ค. ์ด ํตํด ์
๋ ฅ, ์ ์ถ, ์ค๋ฅ ๋ฐ ์ ํจ์ฑ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์์
์ ์ํํ ์ ์์ต๋๋ค. mode: "onChange" ๋ฅผ ์ฌ์ฉํ์ฌ ์
๋ ฅ ๊ฐ์ ๋ณํ๋ฅผ ๊ฐ์งํ๋ฉด์ ๋์์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์คํํฉ๋๋ค. |
setError |
ํผ ์ปจํธ๋กค์ ์ค๋ฅ ์ํ๋ฅผ ์๋์ผ๋ก ์ค์ ๋๋ ๋ณ๊ฒฝํ ์ ์๋ ํจ์์
๋๋ค.name : ์ค๋ฅ ์ํ๋ฅผ ์ค์ ํ๋ ค๋ ํผ ์ปจํธ๋กค์ ์ด๋ฆ.type : ์ค๋ฅ ์ ํ(์: "required", "pattern", "custom" ๋ฑ).message : ์ฌ์ฉ์์๊ฒ ํ์ํ ์ค๋ฅ ๋ฉ์์ง. |
์ ๋ ฅ ํผ | Register๋ฅผ ํตํด value๋ฅผ ์ ์ดํ๊ณ required์ pattern ์ ํตํด API ์ ํจ์ฑ ๊ฒ์ฌ ์ด์ ์ ํจํด ์ ํจ์ฑ๊ฒ์ฌ๋ฅผ ์งํํฉ๋๋ค. |
validate |
pattern ์ ํ์์ ๊ฒ์ฆํ๊ณ validate ๋ ์กฐ๊ฑด์ ๊ฒ์ฆํฉ๋ค |
import { useForm } from "react-hook-form";
const {
register,
handleSubmit,
clearErrors,
setError,
getValues,
formState: { errors, isValid },
} = useForm({
mode: "onChange",
defaultValues: {
// ์ด๊ธฐ๊ฐ
},
});
const checkEmailValid = async email => {
try {
const res = await axios.post(
// ์ด๋ฉ์ผ Validation API
const reqMsg = res.data.message;
clearErrors("email");
if (reqMsg === "์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ ์ฃผ์ ์
๋๋ค.") {
setError("email", {
type: "manual",
message: "์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ ์ฃผ์ ์
๋๋ค.",
});
return false;
} else {
clearErrors("email");
return true;
return (
<StyledForm onSubmit={handleSubmit(handleFormSubmit)}>
<StyledInputContainer>
<StyledLabel htmlFor="email">์ด๋ฉ์ผ</StyledLabel>
<StyledInput
{...register("email", {
required: "์ด๋ฉ์ผ์ ํ์ ์
๋ ฅ์
๋๋ค.",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "์ ํจํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํ์ธ์.",
},
})}
/>
{errors.email && (
<StyledError role="alert">{errors.email.message}</StyledError>
)}
</StyledInputContainer>
// ์ด๋ฉ์ผ ๋ถ๋ถ๊ณผ ๋์ผ
validate: {
matchesPreviousPassword: value => {
const { password } = getValues();
return password === value || "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.";
์ฝ๋ | ์ค๋ช |
---|---|
useEffect useDebounce |
๋ค์์ useEffect ์ useDebounce ๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์ ํค์๋๊ฐ ์
๋ฐ์ดํธ๋ ๋๋ง๋ค ๊ฒ์ API๋ฅผ ํธ์ถํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ํํฐ๋งํ๋ ์ฝ๋์
๋๋ค.useDebounce ๋ ์ผ์ ์๊ฐ ๋์์ ์
๋ ฅ์ด ๋ฉ์ถ ํ API ์์ฒญ์ด ๋ ์ ์๋๋ก ์ ์ดํด ๋ถํ์ํ API ํธ์ถ์ ๋ฐฉ์งํฉ๋๋ค.useEffect ๋ ๋๋ฐ์ด์ค๋ ํค์๋๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๊ฒฐ๊ณผ ๋ชฉ๋ก์ ์
๋ฐ์ดํธํฉ๋๋ค. ์ถ๊ฐ๋ก ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ฐ์ดํฐ๋ฅผ ๊ฑธ๋ฌ๋ด์ด ๋ถํ์ํ ์ด๋ฏธ์ง ๋ก๋ฉ๊ณผ ์ฒ๋ฆฌ์๊ฐ์ ์ค์ผ์ ์๊ฒ ์ค๊ณํ์ต๋๋ค. |
const [debouncedSearchKeyword] = useDebounce(searchKeyword, 300);
useEffect(() => {
const fetchData = async () => {
if (!debouncedSearchKeyword) {
return;
} else {
// ๊ฒ์ API ์ฝ๋ ๋ถ๋ถ
const filteredData = response.data.filter(
item => !item.image.startsWith("https://mandarin.api.weniv"),
);
setSearchListData(filteredData);
}
};
...
fetchData();
}, [debouncedSearchKeyword]);
์ฝ๋ | ์ค๋ช |
---|---|
elapsedTime |
๋๊ธ์ด ์์ฑ๋ ์๊ฐ๊ณผ ํ์ฌ ์๊ฐ ์ฌ์ด์ ๊ฒฝ๊ณผ ์๊ฐ์ ๊ณ์ฐํ์ฌ ๋ฌธ์์ด๋ก ๋ฐํํฉ๋๋ค. ๊ฒฝ๊ณผ ์๊ฐ์ ๋ , ๊ฐ์, ์ผ, ์๊ฐ, ๋ถ ๋ฑ์ ๋จ์๋ก ํํ๋๋ฉฐ, ๊ฐ์ฅ ํฐ ๋จ์๋ถํฐ ๊ณ์ฐ๋๋ฉฐ ๋ฌธ์์ด๋ก ๋ฐํ๋ฉ๋๋ค. |
const elapsedTime = commentDate => {
const now = new Date();
const commentTime = new Date(commentDate);
const elapsedSeconds = Math.floor((now - commentTime) / 1000);
const times = [
{ name: "๋
", seconds: 60 * 60 * 24 * 365 },
{ name: "๊ฐ์", seconds: 60 * 60 * 24 * 30 },
{ name: "์ผ", seconds: 60 * 60 * 24 },
{ name: "์๊ฐ", seconds: 60 * 60 },
{ name: "๋ถ", seconds: 60 },
];
for (const value of times) {
const elapsed = Math.floor(elapsedSeconds / value.seconds);
if (elapsed > 0) {
return `${elapsed}${value.name} ์ `;
}
}
return "๋ฐฉ๊ธ ์ ";
};
์ฝ๋ | ์ค๋ช |
---|---|
const { kakao }= window; |
์นด์นด์ค API์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ๋ค์ ์ฌ์ฉํ ์ ์๋๋ก ๊ตฌํ๋ ๊ฐ์ฒด์ ๋๋ค. |
useState, useEffect | Map ์ปดํฌ๋ํธ ์ด๊ธฐํ, ์ฌ์ฉ์ ์๋, ๊ฒฝ๋์ ๋ฐ๋ฅธ ์ง๋ ๋ง์ปค ํ์ ์ค์ . ์ํ ์ ์ง์ ์ปดํฌ๋ํธ ๋ผ์ดํ ์ฌ์ดํด์ ๋ง์ถฐ ๋์ํ๊ธฐ ์ํด ์ฌ์ฉํฉ๋๋ค. |
๊ฒ์ ์ง๋ ๋ง์ปค ์์ฑ | ๊ฒ์๋ ์์น์ ๋ง์ปค์ ์ค๋ฒ๋ ์ด๋ฅผ ์์ฑํ๊ณ ๊ด๋ จ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. |
const { kakao } = window;
const MapTest = () => {
const [place, setPlace] = useState("");
const [map, setMap] = useState(null);
const location = useLocation();
const data = location.state;
const recommendName = data.restaurantname;
useEffect(() => {
let container = document.getElementById("map");
let options = { center: new kakao.maps.LatLng(37.5045, 127.049) };
let kakaoMap = new kakao.maps.Map(container, options);
setMap(kakaoMap);
}, []);
useEffect(() => {
if (map && recommendName) {
const ps = new kakao.maps.services.Places();
ps.keywordSearch(recommendName, placesSearchCB);
function placesSearchCB(data, status, pagination) {
if (status === kakao.maps.services.Status.OK) {
let bounds = new kakao.maps.LatLngBounds();
for (let i = 0; i < data.length; i++) {
displayMarker(data[i]);
bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
}
map.setBounds(bounds);
}
}
function displayMarker(place) {
const imageSize = new kakao.maps.Size(45, 45);
const imageSrc = Marker;
let markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);
let marker = new kakao.maps.Marker({
// ๋ง์ปค ์ปค์คํ
});
let content =
// ์ง๋ Overlay ์ปค์คํ
setPlace(place.road_address_name);
let customOverlay = new kakao.maps.CustomOverlay({
position: new kakao.maps.LatLng(place.y, place.x),
content: content,
yAnchor: 1,
});
kakao.maps.event.addListener(marker, "click", function () {
customOverlay.setMap(map);
});
}
}
}, [recommendName, map]);
์ฝ๋ | ์ค๋ช |
---|---|
options |
์ด๋ฏธ์ง ์์ถ์ ์ฌ์ฉ๋๋ ์ต์ ์ ์ค์ ํฉ๋๋ค. ์ด๋ฏธ์ง ์ต๋ ํฌ๊ธฐ, ์ต๋ ๋๋น ๋๋ ๋์ด, ์น ์์ ์ ์ฌ์ฉ ์ฌ๋ถ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค. |
์ด๋ฏธ์ง ์์ถ ๋ณํ |
์ ํํ ํ์ผ์ ์์ถํ๊ณ ์ด๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ก ํ์ํ๋ฉฐ, ๋ฐ์ดํฐ URL๋ก ๋ณํํ ํ, ์ด๋ฏธ์ง๋ฅผ ํธ๋ค๋งํ๋๋ฐ ํ์ํ ์ ๋ก๋ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค. |
formDataHandler |
base64 ๋ฐ์ดํฐ URI๋ฅผ blob์ผ๋ก ๋ณํํ๊ณ , ์ด๋ฅผ ๋ค์ File ๊ฐ์ฒด๋ก ๋ณํํ๋ ํจ์์ ๋๋ค. |
import imageCompression from "browser-image-compression";
const options = {
maxSizeMB: 0.7,
maxWidthOrHeight: 500,
useWebWorker: true,
};
try {
const compressedFile = await imageCompression(file, options);
setBoardImage(compressedFile);
const promise = imageCompression.getDataUrlFromFile(compressedFile);
promise.then(result => {
setUploadPreview(result);
});
const reader = new FileReader();
reader.readAsDataURL(compressedFile);
reader.onloadend = () => {
const base64data = reader.result;
const imageUrl = formDataHandler(base64data);
onImageUrlChange(file, imageUrl);
setImgUrl(imageUrl);
};
} catch (error) {
console.log(error);
}
};
const formDataHandler = async dataURI => {
const byteString = atob(dataURI.split(",")[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: "image/jpeg" });
const file = new File([blob], "image.jpg", { type: "image/jpeg" });
return file;
};
- API ๋ถ๋ฆฌ
- ์ ์ญ์ํ๊ด๋ฆฌ(Recoil)๋ฅผ ์ด์ฉํ ๋ชจ๋ฌ ๊ด๋ฆฌ
- ๋ฌดํ ์คํฌ๋กค
- ์ด๋ฏธ์ง ์คํ๋ผ์ดํธ ๊ธฐ๋ฒ
- ์น ์ ๊ทผ์ฑ ๊ฐ์
- sns ๊ณต์ ํ๊ธฐ ๊ธฐ๋ฅ
- ์ด๋ฏธ์ง ์ต๋ 3์ฅ๊น์ง ๋ฑ๋ก(๋๋๊ทธ ์ค ๋๋์ผ๋ก ์์ ๋ณ๊ฒฝ ๊ฐ๋ฅ)
- ํ๋ฒํผ
src
โโ App.js
โโ components
โ โโ Auth
โ โโ Chat
โ โโ Comment
โ โโ common
โ โ โโ Button
โ โ โโ Header
โ โ โโ Nav
โ โโ Error
โ โโ Feed
โ โโ FollowItem
โ โโ Modal
โ โโ Post
โ โ โโ ImgPrev
โ โ โโ PostEdit
โ โ โโ PostItem
โ โ โโ PostList
โ โ โโ StarRating
โ โโ Profile
โ โโ Search
โ โโ styles
โโ pages
โ โโ AuthorPage
โ โ โโ Login
โ โ โโ SignUp
โ โโ Chat
โ โโ Error
โ โโ FollowerList
โ โโ Home
โ โโ Loading
โ โโ Map
โ โโ Post
โ โโ Profile
โ โโ ProfileSetting
โ โโ Search
โ โโ Splash
โ โโ Welcome
โโ routes