본문 바로가기

카테고리 없음

SingleProject_240704 ( Next.js 포켓몬 도감 )

SSR 에서는 url을 절대적으로 입력해야해서 CSR처럼 입력 시 오류가 발생함 -> 전체 주소 입력

export const getPokemon = async (id: string) => {
try {
const response = await axios.get(
`http://localhost:3000/api/pokemons/${id}`
);
return response.data;
} catch (error) {
console.error(`Error fetching pokemon with id ${id}:`, error);
throw new Error("Failed to fetch pokemon");
}
};

 

전체 코드

📦src
 ┣ 📂app
 ┃ ┣ 📂(pokemon)
 ┃ ┃ ┗ 📂_components
 ┃ ┃ ┃ ┣ 📜PokemonDetail.tsx
 ┃ ┃ ┃ ┗ 📜PokemonList.tsx
 ┃ ┣ 📂api
 ┃ ┃ ┗ 📂pokemons
 ┃ ┃ ┃ ┣ 📂[id]
 ┃ ┃ ┃ ┃ ┗ 📜route.ts
 ┃ ┃ ┃ ┗ 📜route.ts
 ┃ ┣ 📂pokemonList
 ┃ ┃ ┗ 📜page.tsx
 ┃ ┣ 📂pokemons
 ┃ ┃ ┗ 📂[id]
 ┃ ┃ ┃ ┗ 📜page.tsx
 ┃ ┣ 📜favicon.ico
 ┃ ┣ 📜globals.css
 ┃ ┣ 📜layout.tsx
 ┃ ┣ 📜page.tsx
 ┃ ┗ 📜provider.tsx
 ┣ 📂constants
 ┃ ┗ 📜constants.ts
 ┗ 📂services
 ┃ ┗ 📜pokemonService.ts

 

import React from "react";
import { getPokemon } from "@/services/pokemonService";
import Link from "next/link";
import Image from "next/image";

type Type = {
  type: { name: string; korean_name: string };
};

type Ability = {
  ability: { name: string; korean_name: string };
};

type Move = {
  move: { name: string; korean_name: string };
};

type Pokemon = {
  id: number;
  name: string;
  korean_name: string;
  height: number;
  weight: number;
  sprites: { front_default: string };
  types: Type[];
  abilities: Ability[];
  moves: Move[];
};

const PokemonDetail = async ({ id }: { id: string }) => {
  const pokemon: Pokemon = await getPokemon(id);

  return (
    <div className="container mx-auto p-4">
      <Image
        src={pokemon.sprites.front_default}
        alt={pokemon.name}
        width={300}
        height={300}
      />
      <h2 className="text-2xl font-bold mb-4">
        이름: {pokemon.korean_name || pokemon.name}
      </h2>

      <p>
        <strong>키:</strong> {`${pokemon.height / 10}m`}
      </p>
      <p>
        <strong>무게:</strong> {`${pokemon.weight / 10}kg`}
      </p>
      <p>
        <strong>타입: </strong>
        {pokemon.types
          .map((type: Type) => type.type.korean_name || type.type.name)
          .join(", ")}
      </p>
      <p>
        <strong>특성: </strong>
        {pokemon.abilities
          .map(
            (ability: Ability) =>
              ability.ability.korean_name || ability.ability.name
          )
          .join(", ")}
      </p>
      <p>
        <strong>기술: </strong>
        {pokemon.moves
          .map((move: Move) => move.move.korean_name || move.move.name)
          .join(", ")}
      </p>
      <Link href="/pokemonList" className="text-blue-500 underline">
        뒤로 가기
      </Link>
    </div>
  );
};

export default PokemonDetail;

 

"use client";

import { useInfiniteQuery } from "@tanstack/react-query";
import React, { useRef, useEffect } from "react";
import { getPokemons } from "@/services/pokemonService";
import Link from "next/link";
import Image from "next/image";
import { TOTAL_POKEMON, POKEMON_PER_PAGE } from "@/constants/constants";

type Pokemon = {
  id: number;
  name: string;
  korean_name: string;
  height: number;
  weight: number;
  sprites: { front_default: string };
  types: { type: { name: string; korean_name: string } }[];
  abilities: { ability: { name: string; korean_name: string } }[];
  moves: { move: { name: string; korean_name: string } }[];
};

const PokemonList = () => {
  const {
    data,
    isLoading,
    isError,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ["pokemons"],
    queryFn: ({ pageParam = 1 }: number) => getPokemons(pageParam),
    getNextPageParam: (lastPage, allPages) => {
      if (allPages.length * POKEMON_PER_PAGE < TOTAL_POKEMON)
        return allPages.length + 1;
      return undefined;
    },
  });

  const observerElem = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    if (observerElem.current) {
      observer.observe(observerElem.current);
    }

    return () => {
      if (observerElem.current) {
        observer.unobserve(observerElem.current);
      }
    };
  }, [fetchNextPage, hasNextPage, isFetchingNextPage]);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error fetching data</div>;
  }

  return (
    <div>
      <h2 className="flex justify-center mb-4">Pokemon List</h2>
      <ul className="grid grid-cols-6 gap-4">
        {data?.pages.flatMap((page) =>
          page.map((pokemon: Pokemon) => (
            <li
              key={pokemon.id}
              className="flex flex-col items-center text-center border rounded shadow"
            >
              <Link href={`/pokemons/${pokemon.id}`}>
                <Image
                  src={pokemon.sprites.front_default}
                  alt={pokemon.name}
                  width={200}
                  height={200}
                />
                <p>{pokemon.korean_name || pokemon.name}</p>
              </Link>
            </li>
          ))
        )}
      </ul>
      <div ref={observerElem} className="h-10 text-xl"></div>
      {isFetchingNextPage && <div>Loading more...</div>}
    </div>
  );
};

export default PokemonList;

 

import { NextResponse } from "next/server";
import axios from "axios";

export const GET = async (
  request: Request,
  { params }: { params: { id: string } }
) => {
  const { id } = params;

  try {
    const response = await axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`);
    const speciesResponse = await axios.get(
    );

    const koreanName = speciesResponse.data.names?.find(
      (name: any) => name.language.name === "ko"
    );

    const typesWithKoreanNames = await Promise.all(
      response.data.types.map(async (type: any) => {
        const typeResponse = await axios.get(type.type.url);
        const koreanTypeName =
          typeResponse.data.names?.find(
            (name: any) => name.language.name === "ko"
          )?.name || type.type.name;
        return { ...type, type: { ...type.type, korean_name: koreanTypeName } };
      })
    );

    const abilitiesWithKoreanNames = await Promise.all(
      response.data.abilities.map(async (ability: any) => {
        const abilityResponse = await axios.get(ability.ability.url);
        const koreanAbilityName =
          abilityResponse.data.names?.find(
            (name: any) => name.language.name === "ko"
          )?.name || ability.ability.name;
        return {
          ...ability,
          ability: { ...ability.ability, korean_name: koreanAbilityName },
        };
      })
    );

    const movesWithKoreanNames = await Promise.all(
      response.data.moves.map(async (move: any) => {
        const moveResponse = await axios.get(move.move.url);
        const koreanMoveName =
          moveResponse.data.names?.find(
            (name: any) => name.language.name === "ko"
          )?.name || move.move.name;
        return { ...move, move: { ...move.move, korean_name: koreanMoveName } };
      })
    );

    const pokemonData = {
      ...response.data,
      korean_name: koreanName?.name || response.data.name,
      types: typesWithKoreanNames,
      abilities: abilitiesWithKoreanNames,
      moves: movesWithKoreanNames,
    };

    return NextResponse.json(pokemonData);
  } catch (error) {
    console.error("Error fetching Pokemon data:", error);
    return NextResponse.json({ error: "Failed to fetch data" });
  }
};

 

import { NextResponse } from "next/server";
import axios from "axios";
import { TOTAL_POKEMON, POKEMON_PER_PAGE } from "@/constants/constants";

export const GET = async (request: Request) => {
  try {
    const url = new URL(request.url);
    const page = parseInt(url.searchParams.get("page") || "1");
    const start = (page - 1) * POKEMON_PER_PAGE + 1;
    const end = Math.min(start + POKEMON_PER_PAGE - 1, TOTAL_POKEMON);

    const allPokemonPromises = Array.from(
      { length: end - start + 1 },
      (_, index) => {
        const id = start + index;
        return Promise.all([
          axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
          axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`),
        ]);
      }
    );

    const allPokemonResponses = await Promise.all(allPokemonPromises);

    const allPokemonData = allPokemonResponses.map(
      ([response, speciesResponse]) => {
        const koreanName = speciesResponse.data.names.find(
          (name: any) => name.language.name === "ko"
        );
        return { ...response.data, korean_name: koreanName?.name || null };
      }
    );

    return NextResponse.json(allPokemonData);
  } catch (error) {
    return NextResponse.json({ error: "Failed to fetch data" });
  }
};

 

import PokemonList from "@/app/(pokemon)/_components/PokemonList";

const PokemonListPage = () => {
  return (
    <div>
      <PokemonList />
    </div>
  );
};

export default PokemonListPage;

 

import PokemonDetail from "@/app/(pokemon)/_components/PokemonDetail";

const PokemonDetailPage = ({
  params: { id },
}: {
  params: {
    id: string;
  };
}) => {
  return (
    <div>
      <PokemonDetail id={id} />
    </div>
  );
};

export default PokemonDetailPage;

 

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import React from "react";
import Link from "next/link";
import QueryProvider from "./provider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "My Pokemon App",
  description: "display Pokemon information",
};

const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
  return (
    <html lang="en">
      <body className={`${inter.className} flex flex-col items-center`}>
        <nav>
          <h1>Welcome to the Pokemon App</h1>
          <Link href="/">Home</Link>
          <Link href="/pokemonList">포켓몬 도감</Link>
        </nav>
        <main className="flex items-center justify-center w-full h-full max-w-4xl">
          <QueryProvider>{children}</QueryProvider>
        </main>
        <footer>
          <p>© 2024 Pokemon App</p>
        </footer>
      </body>
    </html>
  );
};

export default RootLayout;
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React from "react";

function QueryProvider({ children }: React.PropsWithChildren) {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

export default QueryProvider;

 

export const TOTAL_POKEMON = 905;
export const POKEMON_PER_PAGE = 24;

 

import axios from "axios";

export const getPokemons = async (page: number) => {
  try {
    const response = await axios.get(`/api/pokemons?page=${page}`);
    return response.data;
  } catch (error) {
    console.error("Error fetching pokemons:", error);
    throw new Error("Failed to fetch pokemons");
  }
};

export const getPokemon = async (id: string) => {
  try {
    const response = await axios.get(
      `http://localhost:3000/api/pokemons/${id}`
    );
    return response.data;
  } catch (error) {
    console.error(`Error fetching pokemon with id ${id}:`, error);
    throw new Error("Failed to fetch pokemon");
  }
};