24/2/6 최종프로젝트 리뷰(메인상단)
2024. 2. 7. 08:41ㆍ카테고리 없음
//우리 최종의 리뷰를 함 해봅세다.
<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 /> //가게들의 리스트가 들어가는 컴포넌트 오늘은 이3개의 컴포넌트를 보자.
<Best />
<GoodPrice />
<UpButton />
</div>
);
}
<MainBanner.tsx>
import React from "react";
import NowLocationBtn from "./NowLocationBtn"; //현재위치를 나타내는 컴포넌트를 임포트
import Image from "next/image";
import mainBannerImage from "../../app/assets/images/mainBannerImage.jpg"; //배너는 우리 디자이너님의
//예쁜 이미지를 넣었다.export default function MainBanner() {
return (
<div className="relative w-full h-[420px] overflow-hidden max-sm:h-[300px]"> //max-sm 요부분이 반응형인듯?
<Image src={mainBannerImage} alt="mainImage" className="w-full h-full object-cover" />
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center text-white drop-shadow-[4px_0_8px_rgb(0,0,0,0.2)] "> //부모컴포넌트에 relative주시고 여기에 absolute 오호
<div className="text-shadow text-[28px] font-[700] mb-[12px] max-lg:text-[20px] max-sm:text-[15px] max-sm:leading-[20px] max-sm:hidden"> //max... 이부분으로 인해 반응형이 동작하는거 같은데 나중에 물어봐야징
따뜻한 마음들을 모아 당신에게 드려요 :)
</div>
<div className="text-shadow text-[28px] font-[700] mb-[12px] max-lg:text-[20px] max-sm:text-[15px] max-sm:leading-[20px] sm:hidden">
따뜻한 마음들을 모아 <br /> 당신에게 드려요 :)
</div>
<div className="text-shadow text-[40px] font-[700] mb-[32px] max-lg:text-[30px] max-sm:text-[20px] max-sm:leading-[20px]"> //반응형 css를 일단 이런게 있다고 기억해두고 나중에 써먹어보면서 연습해야겄다.
모두의 음식점, 모음
</div>
<NowLocationBtn /> //요거는 현재위치를 검색하는 버튼 컴포넌트
</div>
</div>
);
}
<NowLocationBtn.tsx>
"use client";
import React, { useEffect, useState } from "react";
import Mapinfo from "../detail/Mapinfo"; //아 요컴포넌트는 이제 안쓰니까 지워야되는데 깜빡스 ㅋ
import { useRouter } from "next/navigation";
import useKakaoLoader from "../detail/useKaKao"; //카카오맵 API를 하기위한 세팅중 하나
import { typeOfShop } from "@/app/assets/types/types";
import { useDispatch, useSelector } from "react-redux"; //툴킷 로직
import { getShops } from "@/redux/modules/shopsSlice";
import { RootState } from "@/redux/config/configStore";
// 토스티 import
import { toast, ToastContainer, Slide } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export default function NowLocationBtn() {
const shops = useSelector((state: RootState) => state.allShops); //툴킷에 저장해놓은 가게들 불러오자
const [latitude, setLatitude] = useState<number>(37.49689); //위도 경도를 저장할 유즈스테이트인데
const [longitude, setLongitude] = useState<number>(127.063); //초기값은 강남의 위도경도다.
const [toggle, setToggle] = useState(false); //이것도 이제 필요없어진 로직일텐데 안지웠네?
const dispatch = useDispatch();
useKakaoLoader();
const findNowLoacation = () => { //버튼을 눌렀을때 이벤트 발생 핸들러
if (!window.kakao && !shops) //앞에 윈도우 카카오가 없을때는 빼도 돌아갈듯?
return toast.error("잠시만 기다려주세요", {
transition: Slide, //만약 가게정보가 툴킷에 안담겨 있다면 경고문을 띄운다.
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored"
});
if (window.kakao) { //카카오가 초기세팅이 되있다는 가정아래
const geocoder = new window.kakao.maps.services.Geocoder(); //카카오맵의 지오코더 발동
geocoder.coord2Address(longitude, latitude, function (result, status) { //위도경도로 주소값 찾는 로직
if (status === window.kakao.maps.services.Status.OK) {
const addrArray = result[0].address.address_name.split(" ");//첫번째 결과값의 주소이름을 배열형태로 만듦
const filteredShops = shops.filter((shop: typeOfShop) => {
if (shop.시군 && addrArray[1] && addrArray[0] && shop.시도) { //얘네들이 있다는 가정아래
return (
shop.시군.substring(0, 2) === addrArray[1].substring(0, 2) &&
shop.시도.substring(0, 2) === addrArray[0].substring(0, 2)
); //주소의 첫번째 두번째 음절이 같은 얘들을 필터링한다.
}
});
// console.log(filteredshops, "샵스");
dispatch(getShops(filteredShops)); //이제 그걸 툴킷에 state에 저장해준다.
} else {
console.log("지도 로딩 실패");
}
});
}
};
useEffect(() => { //요 유즈 이펙트는 첫랜더링시랑 툴킷의 allshops가 변할때마다 작동된다.
findNowLoacation();
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition( //요 내장 매서드가 위도 유저의 현재 위도경도를
(position) => { //찾아준다.
setLatitude(position.coords.latitude); //얘네를 유즈 스테이트 위도 경도에 저장
setLongitude(position.coords.longitude); //생각해보니까 findNowLoacation 함수가
}, //저 앞에 있는게 맞나? 유즈스테이트 변경이후에
(error) => { //있어야 더 자연스럽지 않나? 흐음 근데 왜 잘되지?
console.error("Error getting location:", error); /
} //아니다 아니다 저기 있는게 맞다.
); //오히려 유즈 이펙트를 두개 써서 갈라놔야 더 자연스럽겠네
} else { //이거 왜 이렇게 했지 나 빡대가리네
console.error("Geolocation is not supported by your browser");
}
}, [shops]);
return (
<>
<button
className="bg-[#FF8145] hover:bg-[#E5743E] w-[180px] max-sm:w-[130px] text-white py-[14px] px-[10px] rounded-[8px]"
onClick={findNowLoacation} //여기에 있는 온클릭
>
<span className="text-[15px] max-sm:text-[12px]">내 주변 모음 검색하기</span>
</button>
{/* <div>
<h2>Your Location:</h2>
{latitude !== null && longitude !== null ? (
<p>
Latitude: {latitude}, Longitude: {longitude}
</p>
) : (
<p>Loading...</p>
)}
</div> */}
</>
);
}
<SearchForm.tsx> 검색기능 컴포넌트
"use client";
import React, { useCallback, useEffect, useState } from "react"; //유즈콜백을 쓰려했지만 굳이쓸필요 없어서 삭제
import "./home.css";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { getShops } from "@/redux/modules/shopsSlice";
import { typeOfShop } from "@/app/assets/types/types";
import NowLocationBtn from "./NowLocationBtn";
import SigoonOptions from "./SigoonOptions";
import { RootState } from "@/redux/config/configStore";
import { getAllShops } from "@/redux/modules/allShops";
import Image from "next/image";
import down from "../../app/assets/images/icon/down.png";
// 토스티 import
import { toast, ToastContainer, Slide } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import moeumLoading from "../../../src/app/assets/images/moeumLoading.gif";
import { useQuery } from "@tanstack/react-query";
import { getGoodShop } from "./QueryFn";
import { useSession } from "next-auth/react";
export default function SearchForm() {
const dispatch = useDispatch();
// const shops = useSelector((state: RootState) => state.allShops);
// const [shops, setshops] = useState<typeOfShop[]>();
const [form, setForm] = useState({ sido: "", sigoon: "", upzong: "" });//초기값을 빈애들을 줬다.
const { sido, sigoon, upzong } = form; //폼을 구조분해 할당.
const onchangeHandler = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target; //select옵션에 있는 애들을 받는다.
// console.log(name, value);
setForm({ ...form, [name]: value }); //Form을 변경
// console.log(form, "타겟을 확인해 봅시다.");
};
const { data: shops, isLoading } = useQuery({
queryKey: ["allshops"],
queryFn: () => {
return getGoodShop().then((res: typeOfShop[]) => { //getGoodShop은 비동기 통신로지이다.
const filteredRes = res.filter((shop) => {
return ( //어차피 결과값중에서
shop.업종.slice(0, 2) !== "기타" && //업종중에 얘네들은 필요없으니까 out
shop.업종.slice(0, 2) !== "이미" &&
shop.업종.slice(0, 2) !== "목욕" &&
shop.업종.slice(0, 2) !== "세탁"
);
});
dispatch(getAllShops(filteredRes)); //그렇게 필터링 된애들을 바로 툴킷에 넣어준다.
return filteredRes; //이제 그거를 결과값으로 반환 //위에 data: shops에 들어간다.
}); //물론 getGoodShop 앞에도 return 해줘야함
}
});
// useEffect(() => {
// const { data: session } = useSession();
// const userEmail = session?.user?.email || "";
// console.log(userEmail, "들어있나?");
// }, []);
const onClickHandler = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (!sido || !sigoon || !upzong) { //셀렉트 옵션을 모두 선택해주지 않으면 안됨
// toast.error("시도 시군 업종을 선택해주세요");
toast.error("모든 옵션을 선택해주세요.", {
transition: Slide,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored"
});
return; /* setForm({ sido: "", sigoon: "", upzong: "" }); */ //요게 없는게 더 자연스럽다.
}
let filteredShops = shops?.filter((shop) => {
if (shop.시군 && form.sigoon && shop.시도 && form.sido && form.upzong && shop.업종) {
return (
shop.시군.substring(0, 2) === form.sigoon.substring(0, 2) &&
shop.시도.substring(0, 2) === form.sido.substring(0, 2) &&
shop.업종.substring(0, 2) === form.upzong.substring(0, 2)
); //이제 앞에 두글자를 비교한 애들을 필터링 해주자.
}
});
if (!filteredShops?.[0]) //필터링한 애들중에 첫번째 요소가 없으면
return toast.warning("검색결과가 없어요.", {
transition: Slide,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored"
});
dispatch(getShops(filteredShops)); //여기서부턴 결과값이 있으면 툴킷에 넣는다.
toast.success("검색된 모음을 알려드릴게요.", {
transition: Slide,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored"
});
// setForm({ sido: "", sigoon: "", upzong: "" });
};
// console.log(shops, " 샵스");
if (!shops) //가게가 아직 존재하지 않을때
return (
<div className="flex justify-center items-center w-full h-full">
<Image src={moeumLoading} alt="loading" className="w-[300px] h-[300px]" />
</div> //로딩 화면을 보여줄 이미지
);
if (isLoading) {
return <>로딩중</>;
}
return (
<div className="w-full max-md:px-[20px] md:w-[712px] max-md:my-[24px] gap-[12px] md:flex lg:w-[712px] xl:w-[1080px] md:my-[40px]">
<div className="w-full h-[48px] flex gap-4 rounded-xl">
{/* {shops[0].업종} */}
<select
name="sido" //예쁜이름을 붙여줌 ㅋㅋ
onChange={onchangeHandler}
value={form.sido}
className="h-full w-full border-[1px] border-[#C2C2C2] rounded-lg text-[#999] py-[14px] justify-start flex px-[12px]"
>
<option id="none">광역시/도</option>
<option id="1">서울시</option>
<option id="10">부산광역시</option>
<option id="7">대구광역시</option>
<option id="13">인천광역시</option>
<option id="6">광주광역시</option>
<option id="8">대전광역시</option>
<option id="12">울산광역시</option>
<option id="11">세종시</option>
<option id="3">경기도</option>
<option id="2">강원도</option>
<option id="18">충청북도</option>
<option id="17">충청남도</option>
<option id="15">전라북도</option>
<option id="14">전라남도</option>
<option id="5">경상북도</option>
<option id="4">경상남도</option>
<option id="16">제주도</option> //오잉 순서를 바꾸셨네 ㅋㅋ 누군겨?ㅋㅋ
</select>
{/* <select name="upzong" onChange={onchangeHandler} value={form.upzong}> */}
<SigoonOptions name="sigoon" onChange={onchangeHandler} value={form.sigoon} sido={form.sido} />
{/* </select> */} //얘는 별도의 컴포넌트를 만들었는데 앞에 시도에 조건에 따라 다르게 랜더링될예정
<select
name="upzong"
onChange={onchangeHandler}
value={form.upzong}
className="h-full w-full border-[1px] border-[#C2C2C2] rounded-lg text-[#999] py-[14px] justify-start flex px-[12px]"
>
<option id="none">업종</option>
<option>한식</option>
<option>일식</option>
<option>양식</option>
<option>중식</option>
<option>기타</option> //여기 셀렉트 옵션은 별거 없다.앗 기타를 안뺏다 ㅋㅋㅋㅋ
</select>
{/* <NowLocationBtn shops={shops} /> */}
</div>
<section className="w-full flex justify-center items-center md:max-w-[110px]">
<button
className="bg-[#FF8145] hover:bg-[#E5743E] text-[#fff] h-[48px] w-full py-[14px] px-[26px] rounded-[8px] font-[500] md:max-w-[110px] max-md:mt-[12px]"
onClick={onClickHandler}
>
검색하기
</button>
</section>
</div>
);
}
<SigoonOptions.tsx> 스압 주의
"use client";
import { nanoid } from "nanoid";
import React, { useRef, useState } from "react";
const 서울시 = [ //끄앙 하드코딩 했어요 하하 //위에서 언급했던 시도가 뭐냐에 따라 선택될 배열들
"강남구",
"강동구",
"강북구",
"강서구",
"관악구",
"광진구",
"구로구",
"금천구",
"노원구",
"도봉구",
"동대문구",
"동작구",
"마포구",
"서대문구",
"서초구",
"성동구",
"성북구",
"송파구",
"양천구",
"영등포구",
"용산구",
"은평구",
"종로구",
"중구",
"중랑구"
];
const 강원도 = [
"강릉시",
"고성군",
"동해시",
"삼척시",
"속초시",
"양구군",
"양양군",
"영월군",
"원주시",
"인제군",
"정선군",
"철원군",
"춘천시",
"태백시",
"평창군",
"홍천군",
"화천군",
"횡성군"
];
const 경기도 = [
"고양시",
"과천시",
"광명시",
"광주시",
"구리시",
"군포시",
"김포시",
"남양주시",
"동두천시",
"부천시",
"성남시",
"수원시",
"시흥시",
"안산시",
"안성시",
"안양시",
"양주시",
"양평군",
"여주시",
"연천군",
"오산시",
"용인시",
"의왕시",
"의정부시",
"이천시",
"파주시",
"평택시",
"포천시",
"하남시",
"화성시"
];
const 경상남도 = [
"거제시",
"거창군",
"고성군",
"김해시",
"남해군",
"밀양시",
"사천시",
"산청군",
"양산시",
"의령군",
"진주시",
"창녕군",
"창원시",
"통영시",
"하동군",
"함안군",
"함양군",
"합천군"
];
const 경상북도 = [
"경산시",
"경주시",
"고령군",
"구미시",
"군위군",
"김천시",
"문경시",
"봉화군",
"상주시",
"성주군",
"안동시",
"영덕군",
"영양군",
"영주시",
"영천시",
"예천군",
"울릉군",
"울진군",
"의선군",
"청도군",
"청송군",
"칠곡군",
"포항시"
];
const 광주광역시 = ["광산구", "남구", "동구", "북구", "서구"];
const 대구광역시 = ["남구", "달서구", "달성군", "동구", "북구", "서구", "수성구", "중구"];
const 대전광역시 = ["대덕구", "동구", "서구", "유성구", "중구"];
const 부산광역시 = [
"강서구",
"금정구",
"기장군",
"남구",
"동구",
"동래구",
"부산진구",
"북구",
"사상구",
"사하구",
"서구",
"수영구",
"연제구",
"영도구",
"중구",
"해운대구"
];
const 세종시 = [
"가람동",
"고운동",
"금남면",
"나성동",
"다정동",
"대평동",
"도담동",
"반곡동",
"보람동",
"부강면",
"산울동",
"새롬동",
"소담동",
"소정면",
"아름동",
"어진동",
"연기면",
"연동면",
"연서면",
"장군면",
"전동면",
"전의면",
"조치원읍",
"종촌동",
"집현동",
"한솔동",
"합강동",
"해밀동"
];
const 울산광역시 = ["남구", "동구", "북구", "울주군", "중구"];
const 인천광역시 = ["강화군", "계양구", "남동구", "동구", "미추홀구", "부평구", "서구", "연수구", "옹진군", "중구"];
const 전라남도 = [
"강진군",
"고흥군",
"곡성군",
"광양시",
"구례군",
"나주시",
"담양군",
"목포시",
"무안군",
"보성군",
"순천시",
"신안군",
"여수시",
"영광군",
"영암군",
"완도군",
"장성군",
"장흥군",
"진도군",
"함평ㅇ군",
"해남군",
"화순군"
];
const 전라북도 = [
"고창군",
"군산시",
"김제시",
"남원시",
"무주군",
"부안군",
"순창군",
"완주군",
"익산시",
"임실군",
"장수군",
"전주시",
"정읍시",
"진안군"
];
const 제주도 = ["서귀포시", "제주시"];
const 충청남도 = [
"계룡시",
"공주시",
"금산군",
"논산시",
"당진시",
"보령시",
"부여군",
"서산시",
"서천군",
"아산시",
"예산군",
"천안시",
"청양군",
"태안군",
"홍성군"
];
const 충청북도 = [
"괴산군",
"단양군",
"보은군",
"영동군",
"옥천군",
"음성군",
"제천시",
"증평군",
"진천군",
"청주시",
"충주시"
];
export default function SigoonOptions({ //당연히 sido를 프롭스로 받아야 그걸바탕으로 sigoon을 바꾸겠죠?
sido,
name,
onChange,
value
}: {
sido: string;
name: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
value: string;
}) {
const [sigoon, setSigoon] = useState<string[]>([]);
const sigoonRef = useRef<string[]>();
switch (sido) { //스위치 case문으로 sido가 뭐냐에 따라 유즈레프에 다른배열을 넣어준다.
case "경기도":
sigoonRef.current = 경기도;
break;
case "서울시":
sigoonRef.current = 서울시;
break;
case "강원도":
sigoonRef.current = 강원도;
break;
case "경상남도":
sigoonRef.current = 경상남도;
break;
case "경상북도":
sigoonRef.current = 경상북도;
break;
case "광주광역시":
sigoonRef.current = 광주광역시;
break;
case "대구광역시":
sigoonRef.current = 대구광역시;
break;
case "대전광역시":
sigoonRef.current = 대전광역시;
break;
case "부산광역시":
sigoonRef.current = 부산광역시;
break;
case "세종시":
sigoonRef.current = 세종시;
break;
case "울산광역시":
sigoonRef.current = 울산광역시;
break;
case "인천광역시":
sigoonRef.current = 인천광역시;
break;
case "전라남도":
sigoonRef.current = 전라남도;
break;
case "전라북도":
sigoonRef.current = 전라북도;
break;
case "제주도":
sigoonRef.current = 제주도;
break;
case "충청남도":
sigoonRef.current = 충청남도;
break;
case "충청북도":
sigoonRef.current = 충청북도;
break;
}
// console.log(sigoonRef, " 시군찍히나?");
return (
<select
name={name}
onChange={onChange}
value={value}
className="h-full w-full border-[1px] border-[#C2C2C2] rounded-lg text-[#999] py-[14px] justify-start flex px-[12px]"
>
<option id="none">시/군/구</option>
{sigoonRef.current?.map((item) => { //넣어준 배열을 토대로 map함수로 option을 뿌린다.
return <option key={nanoid()}>{item}</option>;
})}
</select>
);
}
//ShopList컴포넌트 부턴 다음시간에 보죠 하하 귀찮은거 아님 절대 아님