From 72177137112fa0944b39f600053d18c171cf8c7c Mon Sep 17 00:00:00 2001 From: PARKGEONTAE <112490505+prkgnt@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:55:26 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20error=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(home)/error.tsx | 31 +++++++++++++++++++++++++ src/app/{error.tsx => global-error.tsx} | 6 +++-- src/app/profile/error.tsx | 31 +++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/app/(home)/error.tsx rename src/app/{error.tsx => global-error.tsx} (69%) create mode 100644 src/app/profile/error.tsx diff --git a/src/app/(home)/error.tsx b/src/app/(home)/error.tsx new file mode 100644 index 0000000..6d8bf2b --- /dev/null +++ b/src/app/(home)/error.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { createPortal } from "react-dom"; +import LoginModal from "../_components/LoginModal/LoginModal"; + +export default function Error({ error }: { error: Error }) { + const router = useRouter(); + const [isLoginModalOpened, setIsLoginModalOpened] = useState(false); + const closeLoginModal = () => { + setIsLoginModalOpened(false); + router.replace("/"); + }; + useEffect(() => { + if (error.message === "Auth Error") { + setIsLoginModalOpened(true); + } + }, []); + + return ( + <> + {isLoginModalOpened && + createPortal( + , + document.body + )} + + ); +} diff --git a/src/app/error.tsx b/src/app/global-error.tsx similarity index 69% rename from src/app/error.tsx rename to src/app/global-error.tsx index ab959e0..59f5341 100644 --- a/src/app/error.tsx +++ b/src/app/global-error.tsx @@ -3,15 +3,17 @@ import { useEffect } from "react"; import styles from "./not-found.module.css"; import Link from "next/link"; +import { useRouter } from "next/navigation"; export default function Error({ error }: { error: Error }) { + const router = useRouter(); useEffect(() => { - console.log(error); + console.log(error.cause, error.message, error.name); }, []); return (
-

서버 에러가 발생했습니다. ㅠㅠ

+

알 수 없는 에러가 발생했습니다. ㅠㅠ

\

location.reload()}> 돌아가기 diff --git a/src/app/profile/error.tsx b/src/app/profile/error.tsx new file mode 100644 index 0000000..6d8bf2b --- /dev/null +++ b/src/app/profile/error.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { createPortal } from "react-dom"; +import LoginModal from "../_components/LoginModal/LoginModal"; + +export default function Error({ error }: { error: Error }) { + const router = useRouter(); + const [isLoginModalOpened, setIsLoginModalOpened] = useState(false); + const closeLoginModal = () => { + setIsLoginModalOpened(false); + router.replace("/"); + }; + useEffect(() => { + if (error.message === "Auth Error") { + setIsLoginModalOpened(true); + } + }, []); + + return ( + <> + {isLoginModalOpened && + createPortal( + , + document.body + )} + + ); +} From 3e45636d34ca733eca3225af1f9aecdab59e3acc Mon Sep 17 00:00:00 2001 From: PARKGEONTAE <112490505+prkgnt@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:55:59 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=B0=8F=20=EC=B5=9C=EC=8B=A0=ED=99=94=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/_hooks/useAuth.ts | 77 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/app/_hooks/useAuth.ts diff --git a/src/app/_hooks/useAuth.ts b/src/app/_hooks/useAuth.ts new file mode 100644 index 0000000..46cdad6 --- /dev/null +++ b/src/app/_hooks/useAuth.ts @@ -0,0 +1,77 @@ +import { ITokenData } from ".."; +import { refresh } from "../_utils/api"; + +const getAccessToken = async () => { + if (typeof window === "undefined") return false; + + const localTokenData = localStorage.getItem("tokenData"); + const now = new Date(); + const localTime = new Date(now.getTime()); + + // 기존에 존재하는 토큰이 있을 때 + if (localTokenData !== null) { + const parsedTokenData: ITokenData = JSON.parse(localTokenData); + // 엑세스 토큰 유효기간이 남아있을 때 + const accessTokenExpiresAt = new Date(parsedTokenData.accessTokenExpiresAt); + const refreshTokenExpiresAt = new Date( + parsedTokenData.refreshTokenExpiresAt + ); + + if (accessTokenExpiresAt > localTime) { + return parsedTokenData.accessToken; + } + // 엑세스 토큰 유효기간 지났고 리프레시 토큰 유효기간 남았을 떄 재발급 + else if ( + accessTokenExpiresAt < localTime && + refreshTokenExpiresAt > localTime + ) { + const tokenData = await refresh(parsedTokenData.refreshToken); + if (tokenData) { + localStorage.setItem("tokenData", JSON.stringify(tokenData)); + return parsedTokenData.accessToken; + } + } + // 모두 유효기간 지났을 때 + } + return false; +}; + +const isLoggedIn = async () => { + if (typeof window === "undefined") return false; + + const localTokenData = localStorage.getItem("tokenData"); + const now = new Date(); + const localTime = new Date(now.getTime()); + + // 기존에 존재하는 토큰이 있을 때 + if (localTokenData !== null) { + const parsedTokenData = JSON.parse(localTokenData); + // 엑세스 토큰 유효기간이 남아있을 때 + const accessTokenExpiresAt = new Date(parsedTokenData.accessTokenExpiresAt); + const refreshTokenExpiresAt = new Date( + parsedTokenData.refreshTokenExpiresAt + ); + + if (accessTokenExpiresAt > localTime) { + return true; + } + // 엑세스 토큰 유효기간 지났고 리프레시 토큰 유효기간 남았을 떄 재발급 + else if ( + accessTokenExpiresAt < localTime && + refreshTokenExpiresAt > localTime + ) { + const tokenData = await refresh(parsedTokenData.refreshToken); + if (tokenData) { + localStorage.setItem("tokenData", JSON.stringify(tokenData)); + return true; + } + } + // 모두 유효기간 지났을 때 + } + return false; +}; +const useAuth = () => { + return { getAccessToken, isLoggedIn }; +}; + +export default useAuth; From d991251817697ab97fc3a3004a8eab64445c78ad Mon Sep 17 00:00:00 2001 From: PARKGEONTAE <112490505+prkgnt@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:56:16 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NewGroupModal/NewGroupModal.tsx | 2 +- .../SaveContentModal/SaveContentModal.tsx | 25 ++----- src/app/_utils/api.ts | 73 ++++++++----------- src/app/groupContents/[id]/page.tsx | 18 ++--- src/app/profile/page.tsx | 9 +-- 5 files changed, 52 insertions(+), 75 deletions(-) diff --git a/src/app/_components/NewGroupModal/NewGroupModal.tsx b/src/app/_components/NewGroupModal/NewGroupModal.tsx index e602bb8..9596d42 100644 --- a/src/app/_components/NewGroupModal/NewGroupModal.tsx +++ b/src/app/_components/NewGroupModal/NewGroupModal.tsx @@ -18,7 +18,7 @@ const NewGroup = ({ const [groupName, setGroupName] = useState(""); const { mutate: createNewGroup } = useMutation({ - mutationFn: () => createGroup(groupName, tokenData.accessToken), + mutationFn: () => createGroup(groupName), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["groupListData"] }), }); diff --git a/src/app/_components/SaveContentModal/SaveContentModal.tsx b/src/app/_components/SaveContentModal/SaveContentModal.tsx index 324c9a1..480cdf4 100644 --- a/src/app/_components/SaveContentModal/SaveContentModal.tsx +++ b/src/app/_components/SaveContentModal/SaveContentModal.tsx @@ -22,21 +22,20 @@ const SaveContent = ({ openNewGroupModal: () => void; contentId: number; }) => { - const localTokenData = localStorage.getItem("tokenData"); - if (localTokenData === null) throw new Error("Token is not found"); - const tokenData = JSON.parse(localTokenData); + // const localTokenData = localStorage.getItem("tokenData"); + // if (localTokenData === null) throw new Error("Token is not found"); + // const tokenData = JSON.parse(localTokenData); const { data: groupListData } = useQuery<{ content: IGroup[] }>({ queryKey: ["groupListData"], - queryFn: () => getGroupList(tokenData.accessToken), - enabled: !!tokenData, + queryFn: () => getGroupList(), + // enabled: !!tokenData, }); const { data: containedGroupList, refetch: refetchContainedGroupList } = useQuery({ queryKey: ["containedGroupList", contentId], - queryFn: () => - getContainedGroupList(contentId.toString(), tokenData.accessToken), + queryFn: () => getContainedGroupList(contentId.toString()), enabled: !!groupListData, }); @@ -50,11 +49,7 @@ const SaveContent = ({ }) => { if (groupListData === undefined) throw new Error("GroupListData is not found"); - return saveContentToGroup( - groupListData.content[index].name, - contentId, - tokenData.accessToken - ); + return saveContentToGroup(groupListData.content[index].name, contentId); }, onSuccess: () => refetchContainedGroupList(), }); @@ -69,11 +64,7 @@ const SaveContent = ({ }) => { if (groupListData === undefined) throw new Error("GroupListData is not found"); - return deleteContentInGroup( - groupListData.content[index].name, - contentId, - tokenData.accessToken - ); + return deleteContentInGroup(groupListData.content[index].name, contentId); }, onSuccess: () => refetchContainedGroupList(), }); diff --git a/src/app/_utils/api.ts b/src/app/_utils/api.ts index 30d77a9..19c4036 100644 --- a/src/app/_utils/api.ts +++ b/src/app/_utils/api.ts @@ -1,8 +1,11 @@ import { IContentData } from ".."; +import useAuth from "../_hooks/useAuth"; export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; const fetchUrl = new URL(BASE_URL || ""); +const { getAccessToken, isLoggedIn } = useAuth(); + function shuffleArray(array: object[]) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -28,19 +31,14 @@ const getContents = async (page: number, searchParams: string) => { fetchUrl.pathname = "/api/content/v1/contents"; fetchUrl.search = `page=${page}&size=10&${searchParams}`; - let tokenData; - - if (typeof window !== "undefined") { - const localData = localStorage.getItem("tokenData"); - if (localData !== null) { - tokenData = JSON.parse(localData); - } - } + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { - headers: tokenData && { - Authorization: `Bearer ${tokenData?.accessToken}`, - }, + headers: access_token + ? { + Authorization: `Bearer ${access_token}`, + } + : undefined, }); if (!data.ok) { @@ -55,19 +53,14 @@ const getContentById = async ( ): Promise => { fetchUrl.pathname = `/api/content/v1/contents/${id}`; - let tokenData; - - if (typeof window !== "undefined") { - const localData = localStorage.getItem("tokenData"); - if (localData !== null) { - tokenData = JSON.parse(localData); - } - } + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { - headers: tokenData && { - Authorization: `Bearer ${tokenData?.accessToken}`, - }, + headers: access_token + ? { + Authorization: `Bearer ${access_token}`, + } + : undefined, }); if (!data.ok) { @@ -209,9 +202,10 @@ const logout = async (refreshToken: string) => { return data; }; -const getGroupList = async (access_token: string) => { +const getGroupList = async () => { fetchUrl.pathname = "/api/bookmark/v1/groups"; + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { headers: { Authorization: `Bearer ${access_token}`, @@ -219,15 +213,19 @@ const getGroupList = async (access_token: string) => { }); if (!data.ok) { + if (data.status === 401) { + throw new Error("Auth Error"); + } throw new Error("API Error"); } return await data.json(); }; -const createGroup = async (groupName: string, access_token: string) => { +const createGroup = async (groupName: string) => { fetchUrl.pathname = "/api/bookmark/v1/groups"; + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { method: "POST", headers: { @@ -276,11 +274,11 @@ const saveContentToGroup_Deprecated = async ( const saveContentToGroup = async ( groupName: string | null, - contentId: number | null, - access_token: string + contentId: number | null ) => { fetchUrl.pathname = `/api/bookmark/v1/groups/${groupName}/contents/${contentId}`; + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { method: "PUT", headers: { @@ -297,13 +295,10 @@ const saveContentToGroup = async ( return data; }; -const getContentListInGroup = async ( - groupName: string, - access_token: string -) => { +const getContentListInGroup = async (groupName: string) => { fetchUrl.pathname = "/api/bookmark/v1/bookmarks"; fetchUrl.search = `groupName=${groupName}`; - + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { headers: { Authorization: `Bearer ${access_token}`, @@ -317,9 +312,9 @@ const getContentListInGroup = async ( return await data.json(); }; -const deleteGroup = async (groupName: string, access_token: string) => { +const deleteGroup = async (groupName: string) => { fetchUrl.pathname = `/api/bookmark/v1/groups/${groupName}`; - + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { method: "DELETE", headers: { @@ -338,11 +333,10 @@ const deleteGroup = async (groupName: string, access_token: string) => { const deleteContentInGroup = async ( groupName: string, - contentId: number | null, - access_token: string + contentId: number | null ) => { fetchUrl.pathname = `/api/bookmark/v1/groups/${groupName}/contents/${contentId}`; - + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { method: "DELETE", headers: { @@ -359,13 +353,10 @@ const deleteContentInGroup = async ( return data; }; -const getContainedGroupList = async ( - contentId: string, - access_token: string -) => { +const getContainedGroupList = async (contentId: string) => { fetchUrl.pathname = "/api/bookmark/v1/groups-with-contains"; fetchUrl.search = `contentId=${contentId}`; - + const access_token = await getAccessToken(); const data = await fetch(fetchUrl.href, { headers: { Authorization: `Bearer ${access_token}`, diff --git a/src/app/groupContents/[id]/page.tsx b/src/app/groupContents/[id]/page.tsx index 7fc7129..bb4635e 100644 --- a/src/app/groupContents/[id]/page.tsx +++ b/src/app/groupContents/[id]/page.tsx @@ -19,11 +19,11 @@ const GroupContents = ({ params }: { params: { id: string } }) => { ); useEffect(() => { const getContentList = async () => { - const localTokenData = localStorage.getItem("tokenData"); - if (localTokenData === null) throw new Error("Token is not found"); - const tokenData = JSON.parse(localTokenData); + // const localTokenData = localStorage.getItem("tokenData"); + // if (localTokenData === null) throw new Error("Token is not found"); + // const tokenData = JSON.parse(localTokenData); const contentsData: { content: IContentData[] } = - await getContentListInGroup(params.id, tokenData.accessToken); + await getContentListInGroup(params.id); setContentsData(contentsData); }; getContentList(); @@ -34,13 +34,13 @@ const GroupContents = ({ params }: { params: { id: string } }) => { ); setContentsData({ content: tempContentsData }); - const localTokenData = localStorage.getItem("tokenData"); - if (localTokenData === null) throw new Error("Token is not found"); - const tokenData = JSON.parse(localTokenData); - await deleteContentInGroup(params.id, contentId, tokenData.accessToken); + // const localTokenData = localStorage.getItem("tokenData"); + // if (localTokenData === null) throw new Error("Token is not found"); + // const tokenData = JSON.parse(localTokenData); + await deleteContentInGroup(params.id, contentId); const newContentsData: { content: IContentData[] } = - await getContentListInGroup(params.id, tokenData.accessToken); + await getContentListInGroup(params.id); setContentsData(newContentsData); }; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 4f2b83f..b5dcd5d 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -17,21 +17,16 @@ import queryClient from "../_utils/queryClient"; const Profile = () => { const router = useRouter(); - const localTokenData = localStorage.getItem("tokenData"); - if (localTokenData === null) throw new Error("Token is not found"); - const tokenData = JSON.parse(localTokenData); - const { data: groupListData, isSuccess: isListDataFetched } = useQuery<{ content: IGroup[]; }>({ queryKey: ["groupListData"], - queryFn: () => getGroupList(tokenData.accessToken), - enabled: !!tokenData, + queryFn: () => getGroupList(), }); const { mutate: deleteGroupFn } = useMutation({ mutationFn: ({ groupName }: { groupName: string }) => - deleteGroup(groupName, tokenData.accessToken), + deleteGroup(groupName), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["groupListData"] }), }); From 517654806eda40f2691d762af933aad819af744c Mon Sep 17 00:00:00 2001 From: PARKGEONTAE <112490505+prkgnt@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:28:49 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EB=B9=A0=EB=A5=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=A0=84=ED=99=98=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20retry=201=EB=A1=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/profile/page.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index b5dcd5d..bf0a206 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,6 +1,5 @@ "use client"; import styles from "./page.module.css"; -import LoginModal from "../_components/LoginModal/LoginModal"; import { deleteGroup, getGroupList } from "../_utils/api"; import { MouseEvent, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; @@ -17,11 +16,16 @@ import queryClient from "../_utils/queryClient"; const Profile = () => { const router = useRouter(); - const { data: groupListData, isSuccess: isListDataFetched } = useQuery<{ + const { + data: groupListData, + isSuccess: isListDataFetched, + isLoading, + } = useQuery<{ content: IGroup[]; }>({ queryKey: ["groupListData"], queryFn: () => getGroupList(), + retry: 1, }); const { mutate: deleteGroupFn } = useMutation({ @@ -31,7 +35,6 @@ const Profile = () => { queryClient.invalidateQueries({ queryKey: ["groupListData"] }), }); - const [isLoginModalOpened, setIsLoginModalOpened] = useState(false); const [isDotMenuOpened, setIsDotMenuOpened] = useState([]); const [isNewGroupModalOpened, setIsNewGroupModalOpened] = useState(false); @@ -70,7 +73,22 @@ const Profile = () => { }; return ( <> - {isLoginModalOpened && } + {isLoading && + createPortal( +
, + document.body + )}

저장한 게시글

From 1cb6130f85af7fe0ec6f44c093d681d0020f8ae4 Mon Sep 17 00:00:00 2001 From: PARKGEONTAE <112490505+prkgnt@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:54:07 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20build=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/_utils/api.ts | 4 +- .../{_hooks/useAuth.ts => _utils/getAuth.ts} | 6 +-- src/app/profile/page.tsx | 39 ++++++++----------- 3 files changed, 22 insertions(+), 27 deletions(-) rename src/app/{_hooks/useAuth.ts => _utils/getAuth.ts} (96%) diff --git a/src/app/_utils/api.ts b/src/app/_utils/api.ts index 19c4036..a1d418c 100644 --- a/src/app/_utils/api.ts +++ b/src/app/_utils/api.ts @@ -1,10 +1,10 @@ import { IContentData } from ".."; -import useAuth from "../_hooks/useAuth"; +import getAuth from "./getAuth"; export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; const fetchUrl = new URL(BASE_URL || ""); -const { getAccessToken, isLoggedIn } = useAuth(); +const { getAccessToken, isLoggedIn } = getAuth(); function shuffleArray(array: object[]) { for (let i = array.length - 1; i > 0; i--) { diff --git a/src/app/_hooks/useAuth.ts b/src/app/_utils/getAuth.ts similarity index 96% rename from src/app/_hooks/useAuth.ts rename to src/app/_utils/getAuth.ts index 46cdad6..0612fd6 100644 --- a/src/app/_hooks/useAuth.ts +++ b/src/app/_utils/getAuth.ts @@ -1,5 +1,5 @@ import { ITokenData } from ".."; -import { refresh } from "../_utils/api"; +import { refresh } from "./api"; const getAccessToken = async () => { if (typeof window === "undefined") return false; @@ -70,8 +70,8 @@ const isLoggedIn = async () => { } return false; }; -const useAuth = () => { +const getAuth = () => { return { getAccessToken, isLoggedIn }; }; -export default useAuth; +export default getAuth; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index bf0a206..8e4f183 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -10,7 +10,6 @@ import plus_gray from "@/../public/assets/plus_gray.svg"; import Image from "next/image"; import NewGroupModal from "../_components/NewGroupModal/NewGroupModal"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { createPortal } from "react-dom"; import queryClient from "../_utils/queryClient"; const Profile = () => { @@ -73,22 +72,20 @@ const Profile = () => { }; return ( <> - {isLoading && - createPortal( -
, - document.body - )} + {isLoading && ( +
+ )}

저장한 게시글

@@ -148,11 +145,9 @@ const Profile = () => {

새 그룹

- {isNewGroupModalOpened && - createPortal( - , - document.body - )} + {isNewGroupModalOpened && ( + + )}