Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 여행 가이드북 디자인, API 연결(무한 스크롤) #15

Merged
merged 16 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 441 additions & 0 deletions public/icons/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/apis/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { GuideBookType } from "@/types";

import { setInstance } from "./axios";

export const getGuideBookData = async (
pageParam: number,
): Promise<GuideBookType> => {
const response = await setInstance(
process.env.NEXT_PUBLIC_GUIDE_BOOK_API_BASE_URL!,
).get(
`/15123631/v1/uddi:33264f0a-158f-4a5d-95cd-99c740c8a097?page=${pageParam}&perPage=20&serviceKey=${process.env.NEXT_PUBLIC_DATA_GO_API_KEY}`,
);
return response.data;
};
10 changes: 6 additions & 4 deletions src/api/axios.ts → src/apis/axios.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import axios from "axios";
import axios, { AxiosInstance } from "axios";

export const instance = axios.create({
baseURL: "baseURL 추가 예정",
headers: {
"Content-Type": "application/json",
},
});

export const setInstance = (baseUrl: string): AxiosInstance => {
instance.defaults.baseURL = baseUrl;
return instance;
};

// 헤더 토큰 추가할 때 사용 등등...
instance.interceptors.request.use();

Expand Down
29 changes: 29 additions & 0 deletions src/components/Loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as S from "./styled";

function Loading() {
return (
<S.FireworksContainer
autostart={true}
options={{
particles: 200, // 입자의 수
traceLength: 10, // 입자의 길이
traceSpeed: 2, // 입자의 속도
delay: { min: 100, max: 100 }, // 입자 생성 지연 시간 범위
brightness: { min: 30, max: 100 }, // 밝기 범위
decay: { min: 0.01, max: 0.02 }, // 소멸 속도 범위
}}
>
<S.Container>
<S.Title>
<S.TitleEffect>FestiBook</S.TitleEffect>
</S.Title>
<S.Text>
<S.TextEffect>데이터를 불러오는 중입니다...</S.TextEffect>
</S.Text>
<S.Spinner />
</S.Container>
</S.FireworksContainer>
);
}

export default Loading;
95 changes: 95 additions & 0 deletions src/components/Loading/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import styled from "@emotion/styled";
import Fireworks from "@fireworks-js/react";

import { MOBILE_MEDIA_QUERY } from "@/styles/const";

export const FireworksContainer = styled(Fireworks)`
width: 100vw;
height: calc(100vh - 5px);
`;

export const Container = styled.div`
@font-face {
font-family: "ONE-Mobile-POP";
src: url("https://fastly.jsdelivr.net/gh/projectnoonnu/[email protected]/ONE-Mobile-POP.woff")
format("woff");
font-weight: normal;
font-style: normal;
}

width: 50rem;
min-width: 23.4375rem;
position: fixed;
top: 47%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "ONE-Mobile-POP";
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;

@media ${MOBILE_MEDIA_QUERY} {
gap: 1rem;
}
`;

export const Title = styled.p`
font-weight: 800;
font-size: 7rem;
background-clip: text;
background: linear-gradient(to right, #e55d87, #5fc3e4) text;

@media ${MOBILE_MEDIA_QUERY} {
font-size: 3.25rem;
}
`;

export const TitleEffect = styled.span`
color: transparent;
-webkit-text-stroke: 0.001px #efefef;
`;

export const Text = styled.p`
font-weight: 800;
font-size: 3rem;
background: linear-gradient(to right, #e55d87, #5fc3e4) text;

@media ${MOBILE_MEDIA_QUERY} {
font-size: 1.75rem;
}
`;

export const TextEffect = styled.span`
letter-spacing: 0.0625rem;
color: transparent;
-webkit-text-stroke: 0.001px #efefef;
`;

export const Spinner = styled.div`
display: flex;
justify-content: center;
align-items: center;
border: 14px solid #e8e8e8;
border-top: 14px solid #ab88af;
border-radius: 50%;
width: 6.25rem;
height: 6.25rem;
animation: spin 1.5s linear infinite;

@media ${MOBILE_MEDIA_QUERY} {
border: 10px solid #f3f3f3;
border-top: 10px solid #ab88af;
width: 4.375rem;
height: 4.375rem;
}

@keyframes spin {
0% {
transform: rotate(0deg); /* 시작 각도 */
}
100% {
transform: rotate(360deg); /* 끝 각도 */
}
}
`;
Empty file removed src/constants/.gitkeep
Empty file.
9 changes: 9 additions & 0 deletions src/constants/queryKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* 쿼리 키 팩토리
* {@link https://tkdodo.eu/blog/effective-react-query-keys}
*/

export const GUIDE_BOOK_KEYS = {
all: ["guide-book"] as const,
lists: () => [...GUIDE_BOOK_KEYS.all, "list"] as const,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가이드북키 상수로 설정해주신거 좋은것같습니다 ! 👍👍👍👍

};
Empty file removed src/hooks/.gitkeep
Empty file.
56 changes: 56 additions & 0 deletions src/hooks/useGetGuideBookData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useRef } from "react";

import { useInfiniteQuery } from "@tanstack/react-query";

import { getGuideBookData } from "@/apis/api";
import { GUIDE_BOOK_KEYS } from "@/constants/queryKey";

export const useGetGuideBookData = () => {
const { data, fetchNextPage, hasNextPage, isLoading, isFetching } =
useInfiniteQuery({
queryKey: GUIDE_BOOK_KEYS.lists(),
queryFn: ({ pageParam }) => getGuideBookData(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
const count = allPages.reduce((acc, cur) => acc + cur.currentCount, 0);

if (count < lastPage.totalCount) {
const pageParam = lastPage.page + 1;
return pageParam;
}

return null;
},
});

const observerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (!entries[0].isIntersecting) {
return;
}

fetchNextPage();
},
{ threshold: 0.5 },
);

if (!observerRef.current) {
return;
}

observer.observe(observerRef.current);

return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetching, isLoading]);

return {
guideBookList: data?.pages,
hasNextPage,
isLoading,
isFetching,
observerRef,
};
};
3 changes: 0 additions & 3 deletions src/pages/404.page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Fireworks from "@fireworks-js/react";

import * as S from "../styles/404.styled";

function NotFound() {
Expand All @@ -14,7 +12,6 @@ function NotFound() {
brightness: { min: 30, max: 100 }, // 밝기 범위
decay: { min: 0.01, max: 0.02 }, // 소멸 속도 범위
}}
style={{ width: "100vw", height: "calc(100vh - 5px)" }}
>
<S.Container>
<S.Title>404</S.Title>
Expand Down
3 changes: 3 additions & 0 deletions src/pages/_app.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ReactElement, ReactNode, useState } from "react";

import { Global } from "@emotion/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

import reset from "@/styles/reset";

Expand Down Expand Up @@ -36,6 +37,8 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
return (
<QueryClientProvider client={queryClient}>
<Global styles={reset} />
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
{getLayout(<Component {...pageProps} />)}
</QueryClientProvider>
);
Expand Down
17 changes: 17 additions & 0 deletions src/pages/guide-book/components/GuideBookItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GuideBookDataListType } from "@/types";

import * as S from "./styled";

interface GuideBookItemProps {
data: GuideBookDataListType;
}

function GuideBookItem({ data }: GuideBookItemProps) {
return (
<S.LinkItem href={data["가이드북 링크"]} target="_blank">
<p>{`제목: ${data["제목"]}`}</p>
</S.LinkItem>
);
}

export default GuideBookItem;
21 changes: 21 additions & 0 deletions src/pages/guide-book/components/GuideBookItem/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from "next/link";

import styled from "@emotion/styled";

export const LinkItem = styled(Link)`
width: 100%;
height: 18rem;
border: 1px solid #000;
`;

export const Frame = styled.iframe`
width: 100%;
height: 18rem;
border: 1px solid #000;
`;

export const Embed = styled.embed`
width: 100%;
height: 18rem;
border: 1px solid #000;
`;
69 changes: 69 additions & 0 deletions src/pages/guide-book/index.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { dehydrate, QueryClient } from "@tanstack/react-query";

import { getGuideBookData } from "@/apis/api";
import Loading from "@/components/Loading";
import { GUIDE_BOOK_KEYS } from "@/constants/queryKey";
import { useGetGuideBookData } from "@/hooks/useGetGuideBookData";

import GuideBookItem from "./components/GuideBookItem";
import * as S from "./styled";

export const getServerSideProps = async () => {
const queryClient = new QueryClient();

try {
await queryClient.prefetchQuery({
queryKey: GUIDE_BOOK_KEYS.lists(),
queryFn: () => getGuideBookData(1),
});

return {
props: { dehydrateState: dehydrate(queryClient) },
};
} catch (error) {
return {
props: { dehydrateState: null },
};
}
};

function GuideBookPage() {
const { hasNextPage, guideBookList, isLoading, isFetching, observerRef } =
useGetGuideBookData();

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

return (
<S.Container>
<S.TitleBox>
<S.Title>여행 가이드북</S.Title>
<S.Line />
</S.TitleBox>
<S.CountBox>{`총 ${guideBookList ? guideBookList[0].totalCount : 0}건`}</S.CountBox>
{guideBookList && guideBookList.length > 0 ? (
<S.ListBox>
{guideBookList.map((list) =>
list.data.map((data) => (
<GuideBookItem key={data.제목} data={data} />
)),
)}
{hasNextPage && !isFetching && <S.Observer ref={observerRef} />}
</S.ListBox>
) : (
<S.EmptyBox>
<S.LogoImage
src="/icons/logo.svg"
width={400}
height={400}
alt="로고 이미지"
/>
가이드북 데이터가 없습니다.
</S.EmptyBox>
)}
</S.Container>
);
}

export default GuideBookPage;
Loading