언어/Next.js

[Next 고캠핑] 드롭다운 선택으로 검색을 해보자 (타입스크립트)

홍시_코딩기록 2024. 7. 25. 22:59

좌) 지역 선택 다 안하고 넘어갔을 경우 경고창/ 우) 검색하고 난 캠핑 리스트 화면

 

 

좌) 캠핑장 리스트 로딩 화면/ 우) 캠핑장 없을 때 <임시 화면>

 

캠핑장 카드가 잘 나와서 검색 기능을 시작했다.

 

 

dropdown.tsx

// 옵션 타입 정의
interface IPropsSelect {
  isMain: boolean;
  onChangeSearch?: (region: string, subRegion: string | null) => void;
}

export default function DropDown({ isMain, onChangeSearch }: IPropsSelect) {
  // 광역시
  const [selectedRegion, setSelectedRegion] =
    useState<SingleValue<Option>>(null);
  // 하위지역
  const [subRegions, setSubRegions] = useState<Option[]>([]);
  // 광역시 선택시 하위지역 리셋
  const [subRegionReset, setSubRegionReset] =
    useState<SingleValue<Option>>(null);
  // 하위지역 disabled
  const [subDisabled, setSubDisabled] = useState(true);

  // 광역시도 onChange
  const onChangeRegion = (selectedOption: SingleValue<Option>) => {
    setSelectedRegion(selectedOption);
    setSubRegionReset(null); // 광역시가 선택되면 서브는 리셋
    if (selectedOption) {
      onChangeSearch?.(selectedOption.value, null); // 지역 검색 값 저장
      
      if (selectedOption.value === "전체") {
        // 전체일 떄는 서브드롭박스를 disabled
        setSubDisabled(true);
      } else {
        // 전체가 아닐 때 서브 드롭박스에 광역시에 맞는 지역으로 매핑
        const subAreas =
          regionMapping[selectedOption.value as keyof typeof regionMapping];
        setSubRegions(createOptions(subAreas));
        setSubDisabled(false);
      }
    } else {
      setSubRegions([]);
    }
  };

  // 하위지역 onChange
  const onChangeSubRegion = (selectedOption: SingleValue<Option>) => {
    setSubRegionReset(selectedOption);
    onChangeSearch?.(
      // 지역 검색 값 저장
      selectedRegion?.value ?? "",
      selectedOption?.value ?? null,
    );
  };

 

- onChangeSerach(광역시, 하위지역)으로 전달될 수 있게 추가하였다.

- 광역시도 onChangeRegion 에도 지역검색값을 저장해주고 (하위지역은 null로)

- 하위지역 onChangeSubRegion 에도 저장

(광역시는 기본값으로 " " 을 넣어줬고 하위지역은 null일 수도 있어서 null을 넣어줬다.)

(광역시를 "전체"로 선택하면 하위지역은 null)

const subAreas =  regionMapping[selectedOption.value as keyof typeof regionMapping];

selectedOption.value = 내가 선택한 광역시도 

키값인 광역시도를 상수타입으로 사용해야하기 때문에 keyof typeof regionMapping을 해줬다.

 

regionMapping이 이런식으로 되어있기 때문에

export const regionMapping = {
  전체: AREA17,
  서울시: AREA1,
  인천시: AREA2,
  대전시: AREA3,
...

regionMapping["대전시"] 이렇게 들어오는 것

그럼 대전시에 맞는 AREA3이 들어오게 subAreas로 들어오게 된다.

 

검색창

index.tsx

export default function Home(): JSX.Element {
  const [region, setRegion] = useState<string>("");
  const [subRegion, setSubRegion] = useState<string | null>(null);
  const router = useRouter();

  const onChangeSearch = (region: string, subRegion: string | null) => {
    setRegion(region);
    setSubRegion(subRegion);
  };

  const onClickList = async (): Promise<void> => {
    if ((region !== null && subRegion !== null) || region === "전체") {
      await router.push({
        pathname: "/campingList",
        query: { region, subRegion },
      });
    } else {
      alert("지역을 선택해주세요!");
    }
  };
  return (
    <>
      <Head>
        <title>Go Camping</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Wrap>
        <MainWrap>
          <Button className="review">요즘 캠핑 후기 보기</Button>
          <SearchBox>
            <h2>Dayily camping</h2>
            <div className="search">
              <DropDown isMain={true} onChangeSearch={onChangeSearch} />
              <Button onClick={onClickList} className="search_btn">
                <span className="mobile">검색하기</span>
                <span className="sr_only">검색</span>
              </Button>
            </div>
          </SearchBox>
        </MainWrap>
      </Wrap>
    </>
  );
}

- onChangeSearch : 광역시, 하위지역 저장해줌

- onClickList: 광역시가 전체를 선택하거나, 드롭박스 두개를 모두 선택할 경우에만 페이지 전환된다.

 query 파라미터로 광역시, 하위지역 전달

 

router.push 사용

https://nextjs.org/docs/pages/api-reference/functions/use-router#router-object

 

Functions: useRouter | Next.js

Learn more about the API of the Next.js Router, and access the router instance in your page with the useRouter hook.

nextjs.org

 

 

campingCardList.tsx

interface ICampingList {
  facltNm: string;
  lineIntro?: string;
  addr1: string;
  firstImageUrl?: string;
  themaEnvrnCl?: string;
  tel: string;
  contentId: number;
  lctCl?: string;
  doNm: string;
  sigunguNm: string;
}

interface IApiResponse {
  response: {
    body: {
      items: {
        item: ICampingList[];
      };
      totalCount: number;
    };
  };
}

interface IPropsList {
  className?: string;
}

export default function CampingCardList({ className }: IPropsList) {
  const [list, setList] = useState<ICampingList[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const [currentPage, setCurrentPage] = useState<number>(1); // 현재 페이지 번호
  const [totalCount, setTotalCount] = useState<number>(0); // 전체 캠핑 아이템 수
  const router = useRouter();
  const { region, subRegion } = router.query;

  const SERVICE_KEY = process.env.NEXT_PUBLIC_SERVICE_KEY;

  // 데이터 불러오기
  useEffect(() => {
    async function fetchData(): Promise<void> {
      setLoading(true);
      try {
        const response = await axios.get<IApiResponse>(
          `https://apis.data.go.kr/B551011/GoCamping/basedList?serviceKey=${SERVICE_KEY}&numOfRows=4000&pageNo=1&MobileOS=AND&MobileApp=appName&_type=json`,
        );

        const items = response.data.response?.body?.items.item || []; // 데이터 없을 경우 추가 수정
        // 지역 필터
        const filteredItems = items.filter((item) => {
          // 전체 지역일 경우
          if (region === "전체") {
            return true;
          }
          // 도는 선택 시는 전체
          if (region !== "전체" && subRegion === "전체") {
            return item.doNm === region;
          }
          // 도 선택 && 시 선택
          if (region !== "전체" && subRegion !== "전체") {
            return item.doNm === region && item.sigunguNm === subRegion;
          }
          return false;
        });
        setTotalCount(filteredItems.length);
        const paginatedItems = filteredItems.slice(
          (currentPage - 1) * PER_PAGE,
          currentPage * PER_PAGE,
        );

        // 데이터 없을 때
        if (paginatedItems.length === 0) {
          setList([]);
        } else {
          setList(paginatedItems);
        }
      } catch (e) {
        console.error(e);
      }
      setLoading(false);
    }
    void fetchData();
  }, [SERVICE_KEY, currentPage]);

  if (loading) {
    return <Loading>대기 중..</Loading>;
  }


  const PER_PAGE = 8; // 한 페이지에 보여줄 아이템 수
  const pageCount = Math.ceil(totalCount / PER_PAGE); // 전체 페이지 수 계산
  const onClickPage = (selected: number) => {
    setCurrentPage(selected);
  };
  return (
    <>
      <Wrap>
        {list.length > 0 ? (
          <CampingCard className="card" list={list} />
        ) : (
          <div>노데이타</div>
        )}
      </Wrap>
      {/* 페이지네이션 */}
      {pageCount > 0 && (
        <Pagination
          totalItems={totalCount}
          onClick={onClickPage}
          currentPage={currentPage}
          pageCount={5}
          itemCountPerPage={PER_PAGE}
        />
      )}
    </>
  );
}

 

- 지금 numOfRows를 4000으로 설정 하고 모든 데이터를 받아와서 필터링하는 방법으로 사용하였다.

이 방법보다 더 나은 방법이 있을 것 같아서 찾아봤지만 고캠핑 api에는 지역 검색하는 파라미터가 없어서 데이터를 우선 모두 받아와서 필터링 하는 방법으로 진행하려 한다... 

- 전체 지역일경우, 도만 선택한경우, 도&시를 선택한 경우로 나눠서 필터링을 해주고

 setTotalCount는 전체 캠핑장 수를 계산해서 페이지네이션 계산,

 paginatedItems는 filterItems를 8개씩 잘라서 보여준다(캠핑장)

 

 

 

 

 

해야 할 것

- 캠핑 리스트 화면에서 드롭다운 기능 쓰기

- 데이터 없음 화면, 로딩 화면

- 좋아요 기능