From e07640b62dd70dca91c9012919dff2bc67394940 Mon Sep 17 00:00:00 2001 From: suzinxix Date: Sat, 11 May 2024 08:28:26 +0900 Subject: [PATCH 01/12] =?UTF-8?q?16=EC=A3=BC=EC=B0=A8=20=EB=AF=B8=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/LoginCheck/LoginCheck.tsx | 20 ++++++ .../common/Modal/FolderModal/FolderModal.tsx | 4 +- .../common/Modal/ShareModal/ShareModal.tsx | 7 +- components/common/Navbar/Navbar.tsx | 41 ++++++----- constants/route.ts | 1 + hooks/useGetFolders.ts | 10 +-- lib/axios.ts | 27 +++++++ pages/Home.module.css | 60 ++++++++++++++++ pages/folder/index.tsx | 6 +- pages/index.tsx | 46 ++++++++---- pages/signin/index.tsx | 67 ++++++++++-------- public/images/bg.svg | 70 +++++++++++++++++++ public/images/bg_home.svg | 28 ++++++++ types/index.ts | 7 +- 14 files changed, 320 insertions(+), 74 deletions(-) create mode 100644 components/common/LoginCheck/LoginCheck.tsx create mode 100644 pages/Home.module.css create mode 100644 public/images/bg.svg create mode 100644 public/images/bg_home.svg diff --git a/components/common/LoginCheck/LoginCheck.tsx b/components/common/LoginCheck/LoginCheck.tsx new file mode 100644 index 000000000..24e3bf7ed --- /dev/null +++ b/components/common/LoginCheck/LoginCheck.tsx @@ -0,0 +1,20 @@ +import { useRouter } from "next/router"; +import { PropsWithChildren, useEffect } from "react"; +import { TOKEN } from "constants/auth"; +import { ROUTE_PATHS } from "constants/route"; + +const LoginCheck = ({ children }: PropsWithChildren) => { + const router = useRouter(); + useEffect(() => { + const accessToken = localStorage.getItem(TOKEN.access); + if (accessToken) { + router.push(ROUTE_PATHS.folder); + } else { + return; + } + }, []); + + return
{children}
; +}; + +export default LoginCheck; diff --git a/components/common/Modal/FolderModal/FolderModal.tsx b/components/common/Modal/FolderModal/FolderModal.tsx index e3c49c866..db21887b3 100644 --- a/components/common/Modal/FolderModal/FolderModal.tsx +++ b/components/common/Modal/FolderModal/FolderModal.tsx @@ -32,10 +32,10 @@ const FolderModal = ({

{link}

diff --git a/components/common/Modal/ShareModal/ShareModal.tsx b/components/common/Modal/ShareModal/ShareModal.tsx index 7b5259e28..5d2f2c936 100644 --- a/components/common/Modal/ShareModal/ShareModal.tsx +++ b/components/common/Modal/ShareModal/ShareModal.tsx @@ -11,7 +11,12 @@ type Props = { }; const ShareModal = ({ isOpen, title, folderName, onCloseClick }: Props) => { - const currentUrl = window.location.href; + + let currentUrl = '' + + if (typeof window !== "undefined") { + currentUrl = window.location.href; + } const copyToClipboard = () => { const textToCopy = currentUrl; diff --git a/components/common/Navbar/Navbar.tsx b/components/common/Navbar/Navbar.tsx index 8c6dc7cea..2f333f6d6 100644 --- a/components/common/Navbar/Navbar.tsx +++ b/components/common/Navbar/Navbar.tsx @@ -1,27 +1,34 @@ -import { useEffect, useState } from "react"; import Link from "next/link"; import styles from "./navbar.module.css"; import Profile from "./Profile/Profile"; -import { fetchGetSampleUsers } from "hooks/useGetSampleData"; import { ROUTE_PATHS } from "constants/route"; -import type { User } from "types"; import Logo from "@/images/logo.svg"; +import useFetch from "hooks/useFetch"; -const ID = 1; +type User = { + id: number; + created_at: string; + name: string; + image_source: string; + email: string; + auth_id: string; +}; function Navbar() { - const [user, setUser] = useState(); + const getUser = () => { + const { data, loading, error } = useFetch<{ data: User[] }>( + ROUTE_PATHS.user + ); + const UserData = data?.data ?? []; - useEffect(() => { - fetchGetSampleUsers(ID) - .then((data: User[]) => { - const [userInfo] = data; - setUser(userInfo); - }) - .catch((err) => { - console.error(err); - }); - }, []); + if (error) { + console.log(error); + } + + return { data: UserData, loading, error }; + }; + + const { data: user, loading, error } = getUser(); return ( ); -} +}; export default Navbar; diff --git a/constants/route.ts b/constants/route.ts index aa6192789..dced482a4 100644 --- a/constants/route.ts +++ b/constants/route.ts @@ -5,5 +5,5 @@ export const ROUTE_PATHS = { login: "/signin", signup: "/signup", folder: "/folder", - shared: "/shoard", -}; + shared: "/shared", +} as const; diff --git a/hooks/useGetUser.ts b/hooks/useGetUser.ts new file mode 100644 index 000000000..06824d642 --- /dev/null +++ b/hooks/useGetUser.ts @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import instance from "lib/axios"; +import useAuthStore from "store/authStore"; +import { ROUTE_PATHS } from "constants/route"; + +interface UserResponse { + id: number; + name: string; + image_source: string; + email: string; +} + +const fetchUser = async (): Promise => { + const { + data: [user], + } = await instance.get("/users"); + return user; +}; + +const useGetUser = () => { + return useQuery({ + queryKey: ["user"], + queryFn: fetchUser, + }); +}; + +export default useGetUser; diff --git a/hooks/useLogin.ts b/hooks/useLogin.ts new file mode 100644 index 000000000..05f9739f6 --- /dev/null +++ b/hooks/useLogin.ts @@ -0,0 +1,40 @@ +import { useMutation } from "@tanstack/react-query"; +import instance from "lib/axios"; +import useAuthStore from "store/authStore"; +import Cookies from "js-cookie"; + +interface User { + email: string; + password: string; +} + +const useLogin = () => { + const setAccessToken = useAuthStore((state) => state.setAccessToken); + + const mutation = useMutation({ + mutationFn: (data: User) => { + return instance.post("/auth/sign-in", data); + }, + onSuccess: (res) => { + // 토큰 저장 + const { accessToken, refreshToken } = res.data; + setAccessToken(accessToken); + Cookies.set("refreshToken", refreshToken, { expires: 7 }); + }, + onError: (error) => { + throw error; + }, + }); + + const login = async (data: User) => { + mutation.mutate(data); + }; + + return { + login, + isPending: mutation.isPending, + isSuccess: mutation.isSuccess, + }; +}; + +export default useLogin; diff --git a/hooks/useSignUp.ts b/hooks/useSignUp.ts new file mode 100644 index 000000000..c820051f6 --- /dev/null +++ b/hooks/useSignUp.ts @@ -0,0 +1,47 @@ +import { useMutation } from "@tanstack/react-query"; + +import instance from "lib/axios"; + +interface User { + email: string; + password: string; +} + +const useSignUp = () => { + const checkEmailMutation = useMutation({ + mutationFn: (email: string) => { + return instance.post("/users/check-email", { email }); + }, + onError: (error) => { + throw error; + }, + }); + + const signUpMutation = useMutation({ + mutationFn: (data: User) => { + return instance.post("/auth/sign-up", data); + }, + onSuccess: (data) => { + // 페이지 이동 + }, + onError: (error) => { + throw error; + }, + }); + + const signUp = async (email: string, password: string) => { + try { + await checkEmailMutation.mutateAsync(email); + await signUpMutation.mutateAsync({ email, password }); + } catch (error) { + throw error; + } + }; + + return { + signUp, + isPending: checkEmailMutation.isPending || signUpMutation.isPending, + }; +}; + +export default useSignUp; diff --git a/lib/axios.ts b/lib/axios.ts index 27e5b711f..8b15696b3 100644 --- a/lib/axios.ts +++ b/lib/axios.ts @@ -4,7 +4,7 @@ import type { AxiosRequestConfig, AxiosError, } from "axios"; -import { TOKEN } from "constants/auth"; +import useAuthStore from "store/authStore"; const instance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -12,20 +12,23 @@ const instance = axios.create({ }); const onRequest = (config: InternalAxiosRequestConfig) => { - // 매 요청마다 localStorage의 토큰을 조회해서 헤더에 추가한다. - if (localStorage.getItem(TOKEN.access)) { - const accessToken = JSON.parse(localStorage.getItem(TOKEN.access) ?? ""); - config.headers.Authorization = accessToken ? `Bearer ${accessToken}` : ""; + const accessToken = useAuthStore.getState().accessToken; + + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; } return config; }; const onError = (error: AxiosError) => { - // Unauthorized 응답을 받으면 가지고 있던 토큰을 제거한다. - if (error.isAxiosError && error.response?.status === 401) { - localStorage.removeItem(TOKEN.access); - console.log(error.response.status); + // Unauthorized 응답 + if (error.isAxiosError) { + if (error.response?.status === 404) { + console.log("존재하지 않는 유저"); + } else { + console.log("인증 오류"); + } } }; diff --git a/pages/_app.tsx b/pages/_app.tsx index bbaaeade9..17ff00fcc 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -18,10 +18,14 @@ type AppPropsWithLayout = AppProps & { export default function App({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page); - return getLayout( + return ( - - + {getLayout( + <> + + + + )} ); } diff --git a/pages/index.tsx b/pages/index.tsx index fcfb1e51f..c218096b9 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,5 @@ import { ReactElement } from "react"; -import Link from "next/link"; import Layout from "@/components/common/Layout/Layout"; -import { ROUTE_PATHS } from "constants/route"; import type { NextPageWithLayout } from "./_app"; import styles from "./Home.module.css"; import Image from "next/image"; diff --git a/pages/signin/index.tsx b/pages/signin/index.tsx index 6cc81b287..882bc48d1 100644 --- a/pages/signin/index.tsx +++ b/pages/signin/index.tsx @@ -13,6 +13,7 @@ import Logo from "@/images/logo.svg"; import { ROUTE_PATHS } from "constants/route"; import { TOKEN } from "constants/auth"; import LoginCheck from "@/components/common/LoginCheck/LoginCheck"; +import useLogin from "hooks/useLogin"; const SignIn = () => { const { @@ -27,26 +28,14 @@ const SignIn = () => { const router = useRouter(); - const postData = async (email: string, password: string) => { + const { login, isPending } = useLogin(); + + const onSubmit: SubmitHandler = async (data) => { try { - const response = await instance.post("/sign-in", { email, password }); - const result = response.data; - return result; + await login(data); + router.push(ROUTE_PATHS.home); } catch (error) { if (axios.isAxiosError(error)) { - throw error; - } - } - }; - - const onSubmit: SubmitHandler = async (data) => { - const { email, password } = data; - postData(email, password) - .then((res) => { - useLocalStorage(TOKEN.access, res.data.accessToken); - router.push(ROUTE_PATHS.folder); - }) - .catch(() => { setError("email", { type: "400", message: "이메일을 확인해 주세요.", @@ -55,7 +44,8 @@ const SignIn = () => { type: "400", message: "비밀번호를 확인해 주세요.", }); - }); + } + } }; return ( diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx index 778077866..550d72cff 100644 --- a/pages/signup/index.tsx +++ b/pages/signup/index.tsx @@ -1,5 +1,4 @@ import { useRouter } from "next/router"; -import instance from "lib/axios"; import axios from "axios"; import { useForm, SubmitHandler } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -7,6 +6,7 @@ import styles from "./signup.module.css"; import { Register, registerSchema } from "lib/zod/schema/RegisterSchema"; import InputField from "@/components/common/InputField/InputField"; import { ROUTE_PATHS } from "constants/route"; +import useSignUp from "hooks/useSignUp"; const SignUp = () => { const { @@ -21,31 +21,30 @@ const SignUp = () => { const router = useRouter(); - const postData = async (email: string, password: string) => { + const { signUp } = useSignUp(); + + const onSubmit: SubmitHandler = async (data) => { + const { email, password } = data; + try { - const response = await instance.post("/sign-up", { email, password }); - const result = response.data; + await signUp(email, password); + router.push(ROUTE_PATHS.login); } catch (error) { if (axios.isAxiosError(error)) { - throw error; + if (error.response?.status === 409) { + setError("email", { + type: "409", + message: "이미 사용 중인 이메일입니다.", + }); + } else { + setError("email", { + message: "다시 시도해 주세요.", + }); + } } } }; - const onSubmit: SubmitHandler = async (data) => { - const { email, password } = data; - postData(email, password) - .then(() => { - router.push(ROUTE_PATHS.folder); - }) - .catch(() => { - setError("email", { - type: "400", - message: "이미 사용 중인 이메일입니다.", - }); - }); - }; - return (
diff --git a/store/authStore.ts b/store/authStore.ts new file mode 100644 index 000000000..7bbc25a18 --- /dev/null +++ b/store/authStore.ts @@ -0,0 +1,22 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface AuthState { + accessToken: string | null; + setAccessToken: (token: string | null) => void; +} + +const useAuthStore = create()( + persist( + (set) => ({ + accessToken: null, + setAccessToken: (token) => set({ accessToken: token }), + }), + { + name: "auth-storage", + getStorage: () => localStorage, + } + ) +); + +export default useAuthStore; From 097b2ce60ee261b12246f161ffa7cd277a50c3e9 Mon Sep 17 00:00:00 2001 From: suzinxix Date: Sun, 12 May 2024 13:55:16 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20fet?= =?UTF-8?q?ch=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=EA=B3=BC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A7=81=ED=81=AC=EC=99=80=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=82=AD=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/Card/Card.tsx | 35 ++++++++++++++++--- components/common/CardList/CardList.tsx | 5 +-- .../common/Modal/DeleteModal/DeleteModal.tsx | 17 +++++---- .../common/Modal/FolderModal/FolderModal.tsx | 4 ++- components/common/Navbar/Navbar.tsx | 10 +++--- components/folder/Category/Category.tsx | 19 +++++----- .../folder/FolderToolBar/FolderToolBar.tsx | 24 +++++++++++-- hooks/useDeleteFolder.ts | 22 ++++++++++++ hooks/useDeleteLink.ts | 22 ++++++++++++ hooks/useGetFolders.ts | 30 +++++++++++----- hooks/useGetLinks.ts | 35 +++++++++++++------ hooks/useGetUser.ts | 6 +--- hooks/useUpdateFolderName.ts | 25 +++++++++++++ pages/folder/index.tsx | 32 +++++++++++++---- pages/index.tsx | 6 +++- types/index.ts | 4 +-- 16 files changed, 229 insertions(+), 67 deletions(-) create mode 100644 hooks/useDeleteFolder.ts create mode 100644 hooks/useDeleteLink.ts create mode 100644 hooks/useUpdateFolderName.ts diff --git a/components/common/Card/Card.tsx b/components/common/Card/Card.tsx index 32de5558a..e2d0a4db6 100644 --- a/components/common/Card/Card.tsx +++ b/components/common/Card/Card.tsx @@ -5,6 +5,8 @@ import styles from "./card.module.css"; import DeleteModal from "@/components/common/Modal/DeleteModal/DeleteModal"; import FolderModal from "@/components/common/Modal/FolderModal/FolderModal"; +import { useDeleteLink } from "hooks/useDeleteLink"; + import { formatDate, getTimeDifference } from "utils/date"; import { MODALS } from "constants/modals"; import type { LinkItem, Folder } from "types"; @@ -12,17 +14,34 @@ import noImage from "@/images/bg_noImage.png"; interface Props { item: LinkItem; + folderId: number; folderList: Folder[] | null; } +export type Link = { + id: number; + favorite: boolean; + created_at: Date; + url: string; + title: string; + image_source: string; + description: string; + [key: string]: number | Date | string | boolean; +}; + // TODO: Card 컴포넌트 분리 -function Card({ item, folderList }: Props) { - const { createdAt, created_at, description, imageSource, image_source, url } = - item; +function Card({ item, folderId, folderList }: Props) { + // const { createdAt, created_at, description, imageSource, image_source, url } = + // item; - const date = createdAt || created_at; + const { id, created_at: date, url, image_source: imgUrl, description } = item; - const imgUrl = imageSource || image_source; + const { + mutateAsync: deleteLink, + isPending, + isError, + error, + } = useDeleteLink(folderId); const absoluteImageUrl = imgUrl?.startsWith("//") ? `https:${imgUrl}` @@ -38,6 +57,11 @@ function Card({ item, folderList }: Props) { setCurrentModal(null); }; + const handleDelete = async () => { + await deleteLink(id); + closeModal(); + }; + const handleCardClick = (url: string) => { window.open(url, "_blank"); }; @@ -137,6 +161,7 @@ function Card({ item, folderList }: Props) { isOpen={currentModal === MODALS.deleteLink} title="폴더 삭제" deletion={url} + onClick={handleDelete} onCloseClick={closeModal} /> diff --git a/components/common/CardList/CardList.tsx b/components/common/CardList/CardList.tsx index 419636eaf..df711bde3 100644 --- a/components/common/CardList/CardList.tsx +++ b/components/common/CardList/CardList.tsx @@ -5,11 +5,12 @@ import NoResults from "@/components/common/NoResults/NoResults"; import type { LinkItem, Folder } from "types"; interface Props { + folderId: number; items: LinkItem[] | null; folderList: Folder[] | null; } -function CardList({ items, folderList }: Props) { +function CardList({ folderId, items, folderList }: Props) { if (!items || items.length === 0) { return ; } @@ -19,7 +20,7 @@ function CardList({ items, folderList }: Props) {
    {items.map((item) => (
  • - +
  • ))}
diff --git a/components/common/Modal/DeleteModal/DeleteModal.tsx b/components/common/Modal/DeleteModal/DeleteModal.tsx index 278d1a925..a14d7da34 100644 --- a/components/common/Modal/DeleteModal/DeleteModal.tsx +++ b/components/common/Modal/DeleteModal/DeleteModal.tsx @@ -6,19 +6,22 @@ type Props = { isOpen: boolean; title: string; deletion: string; + onClick: MouseEventHandler; onCloseClick: MouseEventHandler; }; -const DeleteModal = ({ isOpen, title, deletion, onCloseClick }: Props) => { +const DeleteModal = ({ + isOpen, + title, + deletion, + onClick, + onCloseClick, +}: Props) => { return ( - +

{deletion}

-
diff --git a/components/common/Modal/FolderModal/FolderModal.tsx b/components/common/Modal/FolderModal/FolderModal.tsx index db21887b3..f92a9ad54 100644 --- a/components/common/Modal/FolderModal/FolderModal.tsx +++ b/components/common/Modal/FolderModal/FolderModal.tsx @@ -16,6 +16,7 @@ type Props = { onCloseClick: MouseEventHandler; onKeyDown?: KeyboardEventHandler; onChange?: ChangeEventHandler; + onClick: MouseEventHandler; }; const FolderModal = ({ @@ -25,6 +26,7 @@ const FolderModal = ({ link, folderList, onCloseClick, + onClick, }: Props) => { return ( @@ -43,7 +45,7 @@ const FolderModal = ({ ) : ( )} - diff --git a/components/common/Navbar/Navbar.tsx b/components/common/Navbar/Navbar.tsx index df8f61c89..84637e5b9 100644 --- a/components/common/Navbar/Navbar.tsx +++ b/components/common/Navbar/Navbar.tsx @@ -3,22 +3,20 @@ import styles from "./navbar.module.css"; import Profile from "./Profile/Profile"; import { ROUTE_PATHS } from "constants/route"; import Logo from "@/images/logo.svg"; -import useGetUser from "hooks/useGetUser"; +import { useGetUser } from "hooks/useGetUser"; const Navbar = () => { const { data, isError, isPending } = useGetUser(); - if (isPending) { - return <>; - } - return (