juni
SingleProject_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 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([
]);
}
);
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");
}
};
'프로젝트 > 미니 프로젝트' 카테고리의 다른 글
onboarding ( 로그인 , 회원가입 ) 세팅 (1) | 2024.10.08 |
---|---|
SingleProject_TS-Favorite-Countries (0) | 2024.06.27 |
TeamProject_아웃소싱 프로젝트 (0) | 2024.06.21 |
TeamProject_아웃소싱 프로젝트 (0) | 2024.06.19 |
TeamProject_아웃소싱 프로젝트 (0) | 2024.06.19 |