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.연락처는
    <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 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>
  );
}

// 리팩토링 할게 눈에 보이고 다시 공부도 되고 반응형도 적당히 이해 되고 좋네요