24/2/7 최종프로젝트 리뷰 (메인 중단)
2024. 2. 8. 08:51ㆍ카테고리 없음
<Home.tsx> 어제에 이어 아래 컴포넌트를 봅세다.
import Best from "@/components/home/Best";
import GoodPrice from "@/components/home/GoodPrice";
import MainBanner from "@/components/home/MainBanner";
import SearchForm from "@/components/home/SearchForm";
import ShopList from "@/components/home/ShopList";
import UpButton from "@/components/home/UpButton";
export default function Home() {
return (
<div className="flex flex-col items-center h-[100%] bg-[#fff]">
<MainBanner />
<SearchForm />
<ShopList />
<Best />
<GoodPrice />
<UpButton />
</div>
);
}
<ShopList.tsx>
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { typeOfShop } from "@/app/assets/types/types"; //요기 파일에는 type들을 넣어놨슴다.
import ShopCard from "./ShopCard";
import { nanoid } from "nanoid";
import { RootState } from "@/redux/config/configStore";
import { useRouter } from "next/navigation";
import Image from "next/image";
import place from "../../app/assets/images/icon/place.png";
import map from "../../app/assets/images/icon/map.png";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Scrollbar, Autoplay } from "swiper/modules";
import SwiperCore from "swiper";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import gps from "../../app/assets/images/icon/placeW.png";
export default function ShopList() {
const shops = useSelector((state: RootState) => state.shops);
const dispatch = useDispatch();
// console.log(shops, " 일단 레절트");
const [slide, setSlide] = useState<number>(0); //원래 바닐라 자바스크립트로 만든 캐러셀의 흔적
const router = useRouter(); //앱라우터로 넥스트 쓰시는 분은 임포트 받을때 넥스트 네비게이션에서 받아야함!
// console.log(Math.ceil(shops.length / 4), "길이 알아보자");
// const rigthMove = () => {
// if (Math.ceil((shops.length / 4) * 1000) + slide - 1500 <= 0) return;
// // console.log(Math.ceil(shops.length / 4) * 1000, "흐음", slide);
// setSlide(slide - 500);
// };
// const leftMove = () => {
// if (slide >= 0) return;
// setSlide(slide + 500);
// };
const moveToFullMap = () => {
router.push("/map"); //화면 중앙 하단에 맵으로 가는 버튼이 있는데 그 함수 에요
}; //map이라는 url을 갖는 페이지로 넘어갑니다.
SwiperCore.use([Navigation, Scrollbar, Autoplay]); //팀원이 만들어 주신 캐러셀 이렇게 쓰는거구먼요
return (
<>
<div className="container py-[40px] w-full relative">
<div className="text-center mb-12">
<h1 className="text-[28px] text-[#212121] font-semibold leading-[36px] mb-[12px]">
내 주변의 모음은 어디일까요?
</h1>
<div className="mb-[60px]">
{shops[0]?.시도 === "" ? ( //검색된 배열에 아무것도 없다면 검색해달라는 메시지를 띄어요
<div> 검색이 필요해요</div>
) : (
<div className="flex justify-center gap-2 font-[18px] leading-[26px]">
현재
<Image width={100} height={100} src={place} alt="위치마크" className="w-[24px] h-[24px]" />
<p className="font-bold text-[18px] leading-[26px]">
{shops[0]?.시도} {shops[0]?.시군}
</p> //검색이 정상적으로 됐다면 배열안에 첫번째 요소가
기준이에요 //있을테니까 그 아이로 시도와 시군을 보여주죠
</div>
)}
</div>
</div>
<div className="w-full flex justify-center flex-col items-center">
<div className="swiper-container w-[1080px] max-lg:w-full max-lg:overflow-x-scroll max-lg:hidden">
<Swiper //여기가 캐러셀 라이브러리를 사용한 부분
loop={true} // 슬라이드 루프
spaceBetween={10} // 슬라이스 사이 간격
slidesPerView={4} // 보여질 슬라이스 수
navigation={true} // prev, next button
autoplay={{
delay: 3500,
disableOnInteraction: false // 사용자 상호작용시 슬라이더 일시 정지 비활성
}}
>
{shops.map((shop: typeOfShop) => { //map매서드를 돌리면 return문의 최상단 부모태그에
return ( //key값을 넣어줘야한답니다. 최상단!!
<React.Fragment key={nanoid()}> //React.Fragment 태그는 <></> 얘와 같은애에요
<SwiperSlide key={nanoid()}> //요잉 이거 지금 봤는데 여기에도 키값이 있네요?
<ShopCard shop={shop} shops={shops} /> //Fragment에 key 값을 넣기위해서는
</SwiperSlide> //저렇게 제대로 명명해야합니다.
</React.Fragment> //swiperSlide 를 달아줍니다. 콘솔창에 노란색 경고창이 많이 뜨던데
); //swiperSllide로 인한것으로 여겨지는데 해결을 못했어요
})} //누가 정답을 알려줘~
</Swiper>
</div>
<div className="w-full overflow-x-scroll max-lg:block lg:hidden px-[20px] max-sm:scrollbar-hide ">
<div className="flex cursor-pointer">
{shops.map((shop: typeOfShop) => { //아하 반응형으로 조건부로 위에 shopCard가 보일지
return <ShopCard key={nanoid()} shop={shop} shops={shops} />;
})} //여기 shopCard가 보일지가 결정되는 반응형이네요
</div>
</div>
</div>
<section className="max-sm:hidden">
{shops[0] && ( //검색결과가 있다면 shops배열에 요소의 존재여부로
<div className="fixed z-[200] left-1/2 bottom-[100px] transform -translate-x-1/2">
<button
onClick={moveToFullMap} //요 버튼이 생겨요
className="w-[160px] h-[48px] bg-[#ff8145] rounded-full shadow hover:scale-105 ease-out transition-[1] text-[14px] text-[#fff] flex justify-center items-center gap-[12px] leading-[20px] "
>
지도로 살펴보기
<Image width={100} height={100} className="w-[20px] h-[20px]" src={gps} alt="지도"></Image>
</button>
</div>
)}
</section>
{/* 모바일 맵으로보기 */}
<section className=" max-sm:block sm:hidden"> //역시 반응형으로 화면이 작아지면 요 아래 부분이
{shops[0] && ( //보입니다.
<div className="fixed z-[200] left-1/2 bottom-[20px] transform -translate-x-1/2">
<button //테일윈드로 위치 정해주고요
onClick={moveToFullMap}
className="px-[8px] w-[120px] h-[38px] bg-[#ff8145] rounded-full shadow hover:scale-105 ease-out transition-[1] text-[12px] text-[#fff] flex justify-center items-center gap-[6px] leading-[20px] "
>
지도로 살펴보기
<Image width={100} height={100} className="w-[16px] h-[16px]" src={gps} alt="지도"></Image>
</button>
</div>
)}
</section>
</div>
</>
);
}
<ShopCard.tsx> 샵리스트 안에 있던 shopcard컴포넌트 에요 각각의 카드를 받는 props에 따라 다르게
보여주도록 합니다.
"use client";
import React, { useEffect, useRef, useState } from "react";
import Ddabong from "./Ddabong"; //따봉 컴포넌트
import { useDispatch, useSelector } from "react-redux";
import { useRouter } from "next/navigation";
import { getShop } from "@/redux/modules/detailShopSlice";
import { typeOfShop } from "@/app/assets/types/types";
import { nanoid } from "nanoid";
import Image from "next/image";
import place from "../../app/assets/images/icon/place.png";
import spoon_fork from "../../app/assets/images/icon/spoon_fork.png";
import PhotoOfShop from "./PhotoOfShop";
import { toast, ToastContainer, Slide } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export default function ShopCard({ shop, shops, type }: { shop: typeOfShop; shops?: typeOfShop[]; type?: string }) {
const dispatch = useDispatch(); //props로 shop을 받아서 shop에 따라
// const shops = useSelector((state: any) => state.shops); //다른 정보를 보여줍니다. shops 랑 type은
const router = useRouter(); //과거에 필요했는데 컴포넌트 분리를 하면서
// const [addr, setAddr] = useState({ addrRoad: "", addrBuilding: "" }); //필요없어졌는데 나중에 없앨꼐요 하하
const moveDetailPageBtn = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, phoneNum: string) => {
// console.log(router);
// const detailshop = shops.find((shop: typeOfShop) => {
// return shop.연락처 === phoneNum;
// });
if (!shop?.연락처) //저희 디테일페이지는 연락처를 url로 사용했기때문에 연락처가 없는 가게는 OUT
return toast.warning("상세페이지가 없는 식당입니다.", {
transition: Slide,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored"
});
dispatch(getShop(shop)); //선택된 가게 하나만을 툴킷에 담아둡니다.
router.push(`/detail/${shop.연락처}`); //해당 연락처를 주소값으로 갖는 페이지로 이동
};
// useEffect(() => {
// if (window.kakao) {
// let geocoder = new window.kakao.maps.services.Geocoder();
// geocoder.addressSearch(shop.주소, function (result, status) {
// // console.log(
// // result[0].address_name,
// // result[0].road_address.building_name,
// // "이것좀봅세"
// // );
// if (result[0]) {
// setAddr({
// addrRoad: result[0].address_name,
// addrBuilding: result[0].road_address ? result[0].road_address.building_name : ""
// });
// }
// });
// // const ps = new window.kakao.maps.services.Places();
// // ps.keywordSearch("라페스타", function (result, status) {
// // console.log(result, "이것좀봅세2");
// // });
// }
// }, []);
return ( //예전에는 컴포넌트분리를 안해놔서 shop.연락처를
//인자로 넘겨줘야 했었는데 이제는 필요없어서 shop.연락처는
//인자로 넘겨줘야 했었는데 이제는 필요없어서 shop.연락처는
<button onClick={(e) => moveDetailPageBtn(e, shop.연락처)}> //리팩토링때 없애야 겠네요
<section className="flex w-[252px] bg-[#fff] rounded-lg justify-center items-center mx-[10px]" key={nanoid()}>
<div className="h-full border-opacity-60 rounded-lg w-full">
<PhotoOfShop shop={shop} /> //가게의 이미지만을 갖는 컴포넌트에요
<div className="font-medium text-[#212121] mb-1 flex text-xl">{shop.업소명}</div>
<div className="flex justify-between">
{/* <Ddabong name="thumbup" shopId={shop.연락처} type="small" /> */}
</div>
{/* <div className="flex justify-round gap-5"> */}
<div className="flex gap-1 text-[12px] text-[#5C5C5C] mb-1 items-center">
<Image width={100} height={100} src={spoon_fork} alt="스푼포크" className="w-[18px] h-[18px]" />
{shop.업종}
</div>
<div className="flex gap-1 text-[12px] text-[#5C5C5C] mb-1 items-center ">
<Image width={100} height={100} src={place} alt="위치" className="w-[18px] h-[18px]" />
<span className="w-full text-left block whitespace-nowrap truncate text-ellipsis">{shop.주소}</span>
</div>
{/* </div> */}
</div>
</section>
</button>
);
}
<PhotoOfShop.tsx> shopcard에서 메인 이미지 만을 별도의 컴포넌트로 분리했아요
"use client";
import { typeOfShop } from "@/app/assets/types/types";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import 한식 from "../../app/assets/images/foodIcons/Korea.png";
import 중식 from "../../app/assets/images/foodIcons/China.png";
import 일식 from "../../app/assets/images/foodIcons/Japan.png";
import 양식 from "../../app/assets/images/foodIcons/USA.png";
import { useQuery } from "@tanstack/react-query";
export default function PhotoOfShop({ shop, type }: { shop: typeOfShop; type?: string }) {
const upzong = shop.업종.slice(0, 2); //업종에서 앞에 두글자만을 이용합니다.
let 이미지 = 한식;
switch (upzong) { //그 글자가 무엇이냐에따라 이미지가 결정되요
case "한식":
이미지 = 한식;
break;
case "일식":
이미지 = 일식;
break;
case "중식":
이미지 = 중식;
break;
case "양식":
이미지 = 양식;
break;
}
const getImage = async () => { //이미지를 담아놓은 json서버를 호출합니다.
const getInfo = await fetch("https://raw.githubusercontent.com/hanjiwoo/jsonSERVERforFINAL/main/db.json");
const data = await getInfo.json();
return data.shops;
};
const { data: imageTruck, isLoading } = useQuery({ //이미지 트럭 제맘대로 작명했어요 귀찮았거든요 ㅋㅋ
queryKey: ["shopurl"],
queryFn: getImage //유즈쿼리 기본사용법
});
let foundIMG;
if (imageTruck) { //이미지 트럭안에서 네이밍이 일치하는 요소 하나를 뽑아줍니다.
foundIMG = imageTruck.find((item: { id: number; shopName: string; src: string }) => {
return item.shopName === shop.업소명;
});
}
// if (foundIMG) {
// console.log(foundIMG, "이것도 찍히기를");
// }
if (isLoading) {
return ( //이 포토오브샵 컴포넌트는 비슷한 카드들의 이미지들을 모두 담당하기 때문에
<div //프롭스로 받은 type에 따라 다른 css를 설정합니다.
className={`w-${
type === "best" ? "[344px]" : "[252px]"
} h-[252px] flex justify-center items-center bg-[#FFF2EC] rounded-[12px] mb-[20px] overflow-hidden`}
>
로딩중...
</div>
);
}
return (
<div
className={`w-${
type === "best" ? "[344px]" : "[252px]"
} h-[252px] flex justify-center items-center bg-[#FFF2EC] rounded-[12px] mb-[20px] overflow-hidden`}
>
{foundIMG?.src ? (
<Image
src={foundIMG.src} //일치하는 이미지가 있으면 그 이미지를 띄어주고
alt="크롤링한사진"
width={300}
height={300}
className="h-full w-full object-cover bg-cover "
/>
) : (
<Image //아니라면 위에서 업종에 일치하는 아이를 기본이미지로 띄웁니다.
src={이미지}
alt="음식사진"
width={300}
height={300}
className={`w-${type === "map" ? "[30px]" : "[60px]"} h-${type === "map" ? "[22px]" : "[72px]"}`}
/>
)}
</div>
);
}
<Best.tsx> 홈컴포넌트에서 shoplist다음 컴포넌트에요 요 컴포넌트에서는 ddabong 갯수가 많은
가게 3개 를 보여줍니다.
"use client";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/config/configStore";
import { useQuery } from "@tanstack/react-query";
import { getThumbs } from "./Fns";
import { typeOfShop } from "@/app/assets/types/types";
import { nanoid } from "nanoid";
import ShopCard2 from "./ShopCard2";
export default function Best() {
const shops = useSelector((state: RootState) => state.allShops);
const [top3Shops, setTop3Shops] = useState<(typeOfShop | undefined)[]>(); //가게 3개를 담을 state
const { data: thumbs, isLoading } = useQuery({ //따봉 이라는 파이어베이스의 컬렉션을 하나 가져오는
queryKey: [`thumbs`], //로직이에요
queryFn: getThumbs
});
useEffect(() => { //thumbs와 shops가 변경될때 발동되는 useEffect
const uniqueArray = thumbs?.map((item) => { //thumbs에는 가게 아이디 정보가 있어요
return item.shopId; //그 값을 갖는 배열을만듭니다.
});
// console.log(uniqueArray, "요거는?");
const countThumbs: Record<string, number> = {}; 일단 빈객체를 만듭니다.
uniqueArray?.forEach((element) => { //위에서 만든 배열을 포이치를 돌리고
const key = String(element); //shopId를 키값으로 만들어줍니다. string형식으로요
countThumbs[key] = (countThumbs[key] || 0) + 1; //이제 위에 빈객체에 저 키값에 해당하는 벨류가
}); //이미 있으면 1씩 더해주고 아니라면 1로 시작하게끔합니다.
const entries = Object.entries(countThumbs); //요 entries는 실전에서 처음쓰는데 key와 value를 배열로
const sortedEntries = entries.sort((a, b) => b[1] - a[1]); //갖는 배열을 만들어요
const top3 = sortedEntries.slice(0, 3); //그걸 sort로 value크기에 따라 내림차순해주고
// console.log(top3, "탑3"); //앞에 3개를 잘라줍니다.
const foundTop3 = top3.map((shopid) => { //3개의 배열을 맵함수 돌리고
return shops.find((shop) => { //shops에서 설정한 key값인 shopId와 shops안에 연락처랑 같은애를 찾아요
return shop.연락처 === shopid[0]; //왜냐면 연락처를 id로 사용했거든요 ㅎㅎ
});
});
// console.log(foundTop3, "과연");
if (foundTop3 !== undefined) {
setTop3Shops(foundTop3); //이제 찾은 배열이 존재한다면 그걸 useState에 담아두면
} //사용할 준비끝
}, [thumbs, shops]);
return (
<div className="container px-[20px] mx-auto">
{/* <div style={{ pointerEvents: "none", width: "400px", height: "300px" }}>
<Roadview
position={{
// 지도의 중심좌표
lat: 33.450701,
lng: 126.570667,
radius: 50,
}}
style={{
// 지도의 크기
width: "100%",
height: "100%",
}}
/>
</div> */}
<div className="my-[40px]">
<div className="text-center mb-12">
<h1 className="text-[28px] leading-[36px] text-[#212121] font-semibold mb-[12px]">이달의 Best 매장 모음</h1>
<span className="text-[18px] leading-[20px] font-[#5c5c5c]">인기 매장을 지금 바로 확인해보세요 :)</span>
</div>
<div className="flex justify-center items-center">
<div className="flex w-full gap-[24px] max-lg:overflow-x-scroll max-lg:justify-start max-lg:gap-[20px] justify-center max-sm:scrollbar-hide ">
{top3Shops?.map((shop) => {
return <React.Fragment key={nanoid()}>{shop && <ShopCard2 shop={shop} />}</React.Fragment>;
})}
</div> //사용준비가 된애를 야무지게 뿌려줍니다.
</div> //shopCard2는 위에서 보여드린 shopCard과 거의 비슷해용
</div>
</div>
);
}
// 리팩토링 할게 눈에 보이고 다시 공부도 되고 반응형도 적당히 이해 되고 좋네요